From 8df15fe9f8e06dca895274e1d5bb96dbbefbe4b9 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sat, 30 Oct 2021 20:49:29 -0400 Subject: [PATCH 001/300] Initial contracts creation --- .../extensions/GovernorVotesERC721.sol | 28 ++ .../token/ERC721/extensions/ERC721Votes.sol | 258 ++++++++++++++++++ .../ERC721/extensions/draft-ERC721Permit.sol | 87 ++++++ .../ERC721/extensions/draft-IERC721Permit.sol | 60 ++++ 4 files changed, 433 insertions(+) create mode 100644 contracts/governance/extensions/GovernorVotesERC721.sol create mode 100644 contracts/token/ERC721/extensions/ERC721Votes.sol create mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol create mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol new file mode 100644 index 00000000000..9a2a4ac0ee8 --- /dev/null +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC72GovernorVotesERC721.sol) + +pragma solidity ^0.8.0; + +import "../Governor.sol"; +import "../../token/ERC721/extensions/ERC721Votes.sol"; +import "../../utils/math/Math.sol"; + +/** + * @dev Extension of {Governor} for voting weight extraction from an {ERC721Votes} token. + * + * _Available since v4.3._ + */ +abstract contract ERC72GovernorVotesERC721 is Governor { + ERC721Votes public immutable token; + + constructor(ERC721Votes tokenAddress) { + token = tokenAddress; + } + + /** + * Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). + */ + function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { + return token.getPastVotes(account, blockNumber); + } +} diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol new file mode 100644 index 00000000000..20da35f32a6 --- /dev/null +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/ERC721Votes.sol) + +pragma solidity ^0.8.0; + +import "./draft-ERC721Permit.sol"; +import "../../../utils/math/Math.sol"; +import "../../../utils/math/SafeCast.sol"; +import "../../../utils/cryptography/ECDSA.sol"; + +/** + * @dev Extension of ERC721 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 {ERC721VotesComp} 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 {getVotes} and {getPastVotes}. + * + * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it + * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. + * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this + * will significantly increase the base gas cost of transfers. + * + * _Available since v4.2._ + */ +abstract contract ERC721Votes is ERC721Permit { + struct Checkpoint { + uint32 fromBlock; + uint224 votes; + } + + bytes32 private constant _DELEGATION_TYPEHASH = + keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + + mapping(address => address) private _delegates; + mapping(address => Checkpoint[]) private _checkpoints; + Checkpoint[] private _totalSupplyCheckpoints; + + /** + * @dev Emitted when an account changes their delegate. + */ + event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); + + /** + * @dev Emitted when a token transfer or delegate change results in changes to an account's voting power. + */ + event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); + + /** + * @dev Get the `pos`-th checkpoint for `account`. + */ + function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoint memory) { + return _checkpoints[account][pos]; + } + + /** + * @dev Get number of checkpoints for `account`. + */ + function numCheckpoints(address account) public view virtual returns (uint32) { + return SafeCast.toUint32(_checkpoints[account].length); + } + + /** + * @dev Get the address `account` is currently delegating to. + */ + function delegates(address account) public view virtual returns (address) { + return _delegates[account]; + } + + /** + * @dev Gets the current votes balance for `account` + */ + function getVotes(address account) public view returns (uint256) { + uint256 pos = _checkpoints[account].length; + return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes; + } + + /** + * @dev Retrieve the number of votes for `account` at the end of `blockNumber`. + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) { + require(blockNumber < block.number, "ERC721Votes: block not yet mined"); + return _checkpointsLookup(_checkpoints[account], blockNumber); + } + + /** + * @dev Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances. + * It is but NOT the sum of all the delegated votes! + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastTotalSupply(uint256 blockNumber) public view returns (uint256) { + require(blockNumber < block.number, "ERC721Votes: block not yet mined"); + return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber); + } + + /** + * @dev Lookup a value in a list of (sorted) checkpoints. + */ + function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) private view returns (uint256) { + // We run a binary search to look for the earliest checkpoint taken after `blockNumber`. + // + // During the loop, the index of the wanted checkpoint remains in the range [low-1, high). + // With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant. + // - If the middle checkpoint is after `blockNumber`, we look in [low, mid) + // - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high) + // Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not + // out of bounds (in which case we're looking too far in the past and the result is 0). + // Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is + // past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out + // the same. + uint256 high = ckpts.length; + uint256 low = 0; + while (low < high) { + uint256 mid = Math.average(low, high); + if (ckpts[mid].fromBlock > blockNumber) { + high = mid; + } else { + low = mid + 1; + } + } + + return high == 0 ? 0 : ckpts[high - 1].votes; + } + + /** + * @dev Delegate votes from the sender to `delegatee`. + */ + function delegate(address delegatee) public virtual { + _delegate(_msgSender(), delegatee); + } + + /** + * @dev Delegates votes from signer to `delegatee` + */ + function delegateBySig( + address delegatee, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + require(block.timestamp <= expiry, "ERC721Votes: signature expired"); + address signer = ECDSA.recover( + _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), + v, + r, + s + ); + require(nonce == _useNonce(signer), "ERC721Votes: invalid nonce"); + _delegate(signer, delegatee); + } + + /** + * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). + */ + function _maxSupply() internal view virtual returns (uint224) { + return type(uint224).max; + } + + /** + * @dev Snapshots the totalSupply after it has been increased. + */ + function _mint(address account, uint256 amount) internal virtual override { + super._mint(account, amount);//TODO: update for NFT + require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + + _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); + } + + /** + * @dev Snapshots the totalSupply after it has been decreased. + */ + function _burn(uint256 tokenId) internal virtual override { + super._burn(tokenId); + + _writeCheckpoint(_totalSupplyCheckpoints, _subtract, tokenId); + } + + /** + * @dev Move voting power when tokens are transferred. + * + * Emits a {DelegateVotesChanged} event. + */ + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual { + _moveVotingPower(delegates(from), delegates(to), amount);//TODO: Update to be NFT logic + } + + /** + * @dev Change delegation for `delegator` to `delegatee`. + * + * Emits events {DelegateChanged} and {DelegateVotesChanged}. + */ + function _delegate(address delegator, address delegatee) internal virtual { + address currentDelegate = delegates(delegator); + uint256 delegatorBalance = balanceOf(delegator); + _delegates[delegator] = delegatee; + + emit DelegateChanged(delegator, currentDelegate, delegatee); + + _moveVotingPower(currentDelegate, delegatee, delegatorBalance); + } + + function _moveVotingPower( + address src, + address dst, + uint256 amount + ) private { + if (src != dst && amount > 0) { + if (src != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[src], _subtract, amount); + emit DelegateVotesChanged(src, oldWeight, newWeight); + } + + if (dst != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[dst], _add, amount); + emit DelegateVotesChanged(dst, oldWeight, newWeight); + } + } + } + + function _writeCheckpoint(//TODO: update for NFT + Checkpoint[] storage ckpts, + function(uint256, uint256) view returns (uint256) op, + uint256 delta + ) private returns (uint256 oldWeight, uint256 newWeight) { + uint256 pos = ckpts.length; + oldWeight = pos == 0 ? 0 : ckpts[pos - 1].votes; + newWeight = op(oldWeight, delta); + + if (pos > 0 && ckpts[pos - 1].fromBlock == block.number) { + ckpts[pos - 1].votes = SafeCast.toUint224(newWeight); + } else { + ckpts.push(Checkpoint({fromBlock: SafeCast.toUint32(block.number), votes: SafeCast.toUint224(newWeight)})); + } + } + + function _add(uint256 a, uint256 b) private pure returns (uint256) { + return a + b; + } + + function _subtract(uint256 a, uint256 b) private pure returns (uint256) { + return a - b; + } +} diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol new file mode 100644 index 00000000000..c00d15367ab --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) + +pragma solidity ^0.8.0; + +import "./draft-IERC721Permit.sol"; +import "./ERC721Enumerable.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; +import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/Counters.sol"; + +/** + * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * _Available since v3.4._ + */ +abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { + using Counters for Counters.Counter; + + mapping(address => Counters.Counter) private _nonces; + + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC721 token name. + */ + constructor(string memory name) EIP712(name, "1") {} + + /** + * @dev See {IERC721Permit-permit}. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); + + bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + + bytes32 hash = _hashTypedDataV4(structHash); + + address signer = ECDSA.recover(hash, v, r, s); + require(signer == owner, "ERC721Permit: invalid signature"); + + _approve(spender, value); + } + + /** + * @dev See {IERC721Permit-nonces}. + */ + function nonces(address owner) public view virtual override returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } +} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol new file mode 100644 index 00000000000..61882f2de0d --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + */ +interface IERC721Permit { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC721-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} From fd451479c052c6d890f7608b0f908cccdd24d00d Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 09:28:35 -0400 Subject: [PATCH 002/300] creating permit tests --- contracts/mocks/ERC721PermitMock.sol | 20 + contracts/mocks/ERC721VotesMock.sol | 21 + .../ERC721/extensions/ERC721Votes.test.js | 538 ++++++++++++++++++ .../extensions/draft-ERC721Permit.test.js | 117 ++++ 4 files changed, 696 insertions(+) create mode 100644 contracts/mocks/ERC721PermitMock.sol create mode 100644 contracts/mocks/ERC721VotesMock.sol create mode 100644 test/token/ERC721/extensions/ERC721Votes.test.js create mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol new file mode 100644 index 00000000000..37a860ef4dc --- /dev/null +++ b/contracts/mocks/ERC721PermitMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC721/extensions/draft-ERC721Permit.sol"; + +contract ERC721PermitMock is ERC721Permit { + constructor( + string memory name, + string memory symbol, + address initialAccount, + uint256 tokenId + ) payable ERC721(name, symbol) ERC721Permit(name) { + _mint(initialAccount, tokenId); + } + + function getChainId() external view returns (uint256) { + return block.chainid; + } +} diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol new file mode 100644 index 00000000000..457d05eedb9 --- /dev/null +++ b/contracts/mocks/ERC721VotesMock.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC721/extensions/ERC721Votes.sol"; + +contract ERC721VotesMock is ERC721Votes { + constructor(string memory name, string memory symbol) ERC721(name, symbol) ERC721Permit(name) {} + + function mint(address account, uint256 tokenId) public { + _mint(account, tokenId); + } + + function burn(uint256 tokenId) public { + _burn(tokenId); + } + + function getChainId() external view returns (uint256) { + return block.chainid; + } +} diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js new file mode 100644 index 00000000000..7078828039d --- /dev/null +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -0,0 +1,538 @@ +/* eslint-disable */ + +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const { promisify } = require('util'); +const queue = promisify(setImmediate); + +const ERC721VotesMock = artifacts.require('ERC721VotesMock'); + +const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); + +const Delegation = [ + { name: 'delegatee', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'expiry', type: 'uint256' }, +]; + +async function countPendingTransactions() { + return parseInt( + await network.provider.send('eth_getBlockTransactionCountByNumber', ['pending']) + ); +} + +async function batchInBlock (txs) { + try { + // disable auto-mining + await network.provider.send('evm_setAutomine', [false]); + // send all transactions + const promises = txs.map(fn => fn()); + // wait for node to have all pending transactions + while (txs.length > await countPendingTransactions()) { + await queue(); + } + // mine one block + await network.provider.send('evm_mine'); + // fetch receipts + const receipts = await Promise.all(promises); + // Sanity check, all tx should be in the same block + const minedBlocks = new Set(receipts.map(({ receipt }) => receipt.blockNumber)); + expect(minedBlocks.size).to.equal(1); + + return receipts; + } finally { + // enable auto-mining + await network.provider.send('evm_setAutomine', [true]); + } +} + +contract('ERC721Votes', function (accounts) { + const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; + const supply = new BN('10000000000000000000000000'); + + beforeEach(async function () { + this.token = await ERC721VotesMock.new(name, symbol); + + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = await this.token.getChainId(); + }); + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect( + await this.token.DOMAIN_SEPARATOR(), + ).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address), + ); + }); + + it('minting restriction', async function () { + const amount = new BN('2').pow(new BN('224')); + await expectRevert( + this.token.mint(holder, amount), + 'ERC721Votes: total supply risks overflowing votes', + ); + }); + + describe('set delegation', function () { + describe('call', function () { + it('delegation with balance', async function () { + await this.token.mint(holder, supply); + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + + expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); + }); + + it('delegation without balance', async function () { + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + }); + }); + + describe('with signature', function () { + const delegator = Wallet.generate(); + const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); + const nonce = 0; + + const buildData = (chainId, verifyingContract, message) => ({ data: { + primaryType: 'Delegation', + types: { EIP712Domain, Delegation }, + domain: { name, version, chainId, verifyingContract }, + message, + }}); + + beforeEach(async function () { + await this.token.mint(delegatorAddress, supply); + }); + + it('accept signed delegation', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + expectEvent(receipt, 'DelegateChanged', { + delegator: delegatorAddress, + fromDelegate: ZERO_ADDRESS, + toDelegate: delegatorAddress, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: delegatorAddress, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); + + expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); + }); + + it('rejects reused signature', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), + 'ERC721Votes: invalid nonce', + ); + }); + + it('rejects bad delegatee', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + const { logs } = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); + const { args } = logs.find(({ event }) => event == 'DelegateChanged'); + expect(args.delegator).to.not.be.equal(delegatorAddress); + expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); + expect(args.toDelegate).to.be.equal(holderDelegatee); + }); + + it('rejects bad nonce', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), + 'ERC721Votes: invalid nonce', + ); + }); + + it('rejects expired permit', async function () { + const expiry = (await time.latest()) - time.duration.weeks(1); + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry, + }), + )); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), + 'ERC721Votes: signature expired', + ); + }); + }); + }); + + describe('change delegation', function () { + beforeEach(async function () { + await this.token.mint(holder, supply); + await this.token.delegate(holder, { from: holder }); + }); + + it('call', async function () { + expect(await this.token.delegates(holder)).to.be.equal(holder); + + const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: holder, + toDelegate: holderDelegatee, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: supply, + newBalance: '0', + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holderDelegatee, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); + + expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); + expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); + }); + }); + + 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.getVotes(holder)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); + + // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" + const blockNumber = await time.latestBlock(); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getPastVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes); + }); + }); + + // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js. + describe('Compound test suite', function () { + beforeEach(async function () { + await this.token.mint(holder, supply); + }); + + describe('balanceOf', function () { + it('grants to initial account', async function () { + expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); + }); + }); + + describe('numCheckpoints', function () { + it('returns the number of checkpoints for a delegate', async function () { + await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const t1 = await this.token.delegate(other1, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + + const t2 = await this.token.transfer(other2, 10, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + + const t3 = await this.token.transfer(other2, 10, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); + + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '100' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '90' ]); + expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '80' ]); + expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + + await time.advanceBlock(); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); + }); + + it('does not add more than one checkpoint in a block', async function () { + await this.token.transfer(recipient, '100', { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const [ t1, t2, t3 ] = await batchInBlock([ + () => this.token.delegate(other1, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + ]); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); + // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check + // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + }); + }); + + describe('getPastVotes', function () { + it('reverts if block number >= current block', async function () { + await expectRevert( + this.token.getPastVotes(other1, 5e10), + 'ERC721Votes: block not yet mined', + ); + }); + + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.transfer(holder, 20, { from: other2 }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + }); + }); + + describe('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), + 'ERC721Votes: block not yet mined', + ); + }); + + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + t1 = await this.token.mint(holder, supply); + + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const t1 = await this.token.mint(holder, supply); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.mint(holder, supply); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.burn(10); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.burn(10); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.mint(holder, 20); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + }); +}); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js new file mode 100644 index 00000000000..7a7b8cf5d83 --- /dev/null +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -0,0 +1,117 @@ +/* eslint-disable */ + +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const ERC721PermitMock = artifacts.require('ERC721PermitMock'); + +const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); + +const Permit = [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, +]; + +contract('ERC721Permit', function (accounts) { + const [ initialHolder, spender, recipient, other ] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; + + const initialTokenId = new BN('100'); + + beforeEach(async function () { + this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); + + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = await this.token.getChainId(); + }); + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect( + await this.token.DOMAIN_SEPARATOR(), + ).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address), + ); + }); + + describe.only('permit', function () { + const wallet = Wallet.generate(); + + const owner = wallet.getAddressString(); + const value = initialTokenId; + const nonce = 0; + const maxDeadline = MAX_UINT256; + + const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ + primaryType: 'Permit', + types: { EIP712Domain, Permit }, + domain: { name, version, chainId, verifyingContract }, + message: { owner, spender, value, nonce, deadline }, + }); + + it('accepts owner signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); + expect(await this.token.getApproved(value)).to.be.equal(spender); + }); + + it('rejects reused signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects other signature', async function () { + const otherWallet = Wallet.generate(); + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects expired permit', async function () { + const deadline = (await time.latest()) - time.duration.weeks(1); + + const data = buildData(this.chainId, this.token.address, deadline); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, deadline, v, r, s), + 'ERC721Permit: expired deadline', + ); + }); + }); +}); From 80e3c28338186a35a9bc28151ab150899e48aa64 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 10:26:22 -0400 Subject: [PATCH 003/300] Fixing checkpoints count --- .../token/ERC721/extensions/ERC721Votes.sol | 11 ++--- .../ERC721/extensions/ERC721Votes.test.js | 47 ++++++++++++------- .../extensions/draft-ERC721Permit.test.js | 2 +- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 20da35f32a6..b184c9c83fa 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -7,7 +7,6 @@ import "./draft-ERC721Permit.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; - /** * @dev Extension of ERC721 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. @@ -170,11 +169,11 @@ abstract contract ERC721Votes is ERC721Permit { /** * @dev Snapshots the totalSupply after it has been increased. */ - function _mint(address account, uint256 amount) internal virtual override { - super._mint(account, amount);//TODO: update for NFT + function _mint(address account, uint256 tokenId) internal virtual override { + super._mint(account, tokenId); require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } /** @@ -183,7 +182,7 @@ abstract contract ERC721Votes is ERC721Permit { function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); - _writeCheckpoint(_totalSupplyCheckpoints, _subtract, tokenId); + _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); } /** @@ -232,7 +231,7 @@ abstract contract ERC721Votes is ERC721Permit { } } - function _writeCheckpoint(//TODO: update for NFT + function _writeCheckpoint( Checkpoint[] storage ckpts, function(uint256, uint256) view returns (uint256) op, uint256 delta diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 7078828039d..d2981dd5f9d 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -54,7 +54,9 @@ async function batchInBlock (txs) { contract('ERC721Votes', function (accounts) { const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; - + const NFT1 = new BN('10'); + const NFT2 = new BN('20'); + const NFT3 = new BN('30'); const name = 'My Token'; const symbol = 'MTKN'; const version = '1'; @@ -91,7 +93,7 @@ contract('ERC721Votes', function (accounts) { describe('set delegation', function () { describe('call', function () { - it('delegation with balance', async function () { + it('delegation with balance', async function () {//TODO: Make it NFT like await this.token.mint(holder, supply); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); @@ -249,7 +251,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, supply);//TODO: Avoid tokenId duplicate await this.token.delegate(holder, { from: holder }); }); @@ -359,6 +361,9 @@ contract('ERC721Votes', function (accounts) { describe('Compound test suite', function () { beforeEach(async function () { await this.token.mint(holder, supply); + await this.token.mint(holder, NFT1); + await this.token.mint(holder, NFT2); + await this.token.mint(holder, NFT3); }); describe('balanceOf', function () { @@ -448,16 +453,16 @@ contract('ERC721Votes', function (accounts) { }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); + const t1 = await this.token.delegate(other1, { from: holder });//TODO: Make it NFT like 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 }); + const t3 = await this.token.transfer(other2, 20, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 20, { from: other2 }); + const t4 = await this.token.transfer(holder, 30, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); @@ -511,28 +516,34 @@ contract('ERC721Votes', function (accounts) { }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.mint(holder, supply); + const t1 = await this.token.mint(holder, NFT1); await time.advanceBlock(); await time.advanceBlock(); - const t2 = await this.token.burn(10); + const t2 = await this.token.burn(NFT1); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.burn(10); + const t3 = await this.token.mint(holder, NFT2); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.mint(holder, 20); + const t4 = await this.token.burn(NFT2); + await time.advanceBlock(); + await time.advanceBlock(); + const t5 = await this.token.mint(holder, NFT3); await time.advanceBlock(); await time.advanceBlock(); + console.log(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); }); }); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js index 7a7b8cf5d83..f22c904c724 100644 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -50,7 +50,7 @@ contract('ERC721Permit', function (accounts) { ); }); - describe.only('permit', function () { + describe('permit', function () { const wallet = Wallet.generate(); const owner = wallet.getAddressString(); From 72319825180f08b4b9b843c48ae35ce195271b02 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:20:14 -0400 Subject: [PATCH 004/300] Updating ERC721Vote tests --- contracts/mocks/ERC721VotesMock.sol | 4 + .../token/ERC721/extensions/ERC721Votes.sol | 18 ++- .../ERC721/extensions/ERC721Votes.test.js | 146 +++++++++--------- 3 files changed, 96 insertions(+), 72 deletions(-) diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 457d05eedb9..09e40a4ea85 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -18,4 +18,8 @@ contract ERC721VotesMock is ERC721Votes { function getChainId() external view returns (uint256) { return block.chainid; } + + function _maxSupply() internal pure override returns(uint224){ + return uint224(4); + } } diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index b184c9c83fa..146ff1dd213 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -192,10 +192,9 @@ abstract contract ERC721Votes is ERC721Permit { */ function _afterTokenTransfer( address from, - address to, - uint256 amount + address to ) internal virtual { - _moveVotingPower(delegates(from), delegates(to), amount);//TODO: Update to be NFT logic + _moveVotingPower(delegates(from), delegates(to), 1); } /** @@ -254,4 +253,17 @@ abstract contract ERC721Votes is ERC721Permit { function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } + + /** + * @dev Moves token from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 tokenId) external returns (bool){ + _transfer(_msgSender(), recipient, tokenId); + _afterTokenTransfer(_msgSender(), recipient); + return true; + } } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index d2981dd5f9d..b2d56ea7ab0 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -14,6 +14,7 @@ const queue = promisify(setImmediate); const ERC721VotesMock = artifacts.require('ERC721VotesMock'); const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); +const { Console } = require('console'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -84,9 +85,14 @@ contract('ERC721Votes', function (accounts) { }); it('minting restriction', async function () { - const amount = new BN('2').pow(new BN('224')); + const lastTokenId = new BN('2').pow(new BN('224')); + this.token.mint(holder, NFT1); + this.token.mint(holder, NFT2); + this.token.mint(holder, NFT3); + this.token.mint(holder, supply); + await expectRevert( - this.token.mint(holder, amount), + this.token.mint(holder, lastTokenId), 'ERC721Votes: total supply risks overflowing votes', ); }); @@ -106,15 +112,15 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '0', - newBalance: supply, + newBalance: '1', }); expect(await this.token.delegates(holder)).to.be.equal(holder); - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); + expect(await this.token.getVotes(holder)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); }); it('delegation without balance', async function () { @@ -169,15 +175,15 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: delegatorAddress, previousBalance: '0', - newBalance: supply, + newBalance: '1', }); expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); - expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); + expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1'); }); it('rejects reused signature', async function () { @@ -251,7 +257,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { - await this.token.mint(holder, supply);//TODO: Avoid tokenId duplicate + await this.token.mint(holder, supply); await this.token.delegate(holder, { from: holder }); }); @@ -266,24 +272,24 @@ contract('ERC721Votes', function (accounts) { }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, - previousBalance: supply, + previousBalance: '1', newBalance: '0', }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holderDelegatee, previousBalance: '0', - newBalance: supply, + newBalance: '1', }); expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); - expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); + expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); }); }); @@ -293,8 +299,8 @@ contract('ERC721Votes', function (accounts) { }); it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -304,22 +310,22 @@ contract('ERC721Votes', function (accounts) { 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 { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); 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.holderVotes = '0'; 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' }); + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -333,15 +339,15 @@ contract('ERC721Votes', function (accounts) { 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) }); + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); 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.holderVotes = '0'; this.recipientVotes = '1'; }); @@ -368,54 +374,55 @@ contract('ERC721Votes', function (accounts) { describe('balanceOf', function () { it('grants to initial account', async function () { - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('4'); }); }); 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 + + await this.token.transfer(recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transfer(recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - - const t2 = await this.token.transfer(other2, 10, { from: recipient }); + + const t2 = await this.token.transfer(other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - - const t3 = await this.token.transfer(other2, 10, { from: recipient }); + + const t3 = await this.token.transfer(other2, NFT2, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - const t4 = await this.token.transfer(recipient, 20, { from: holder }); + const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '100' ]); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '90' ]); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '80' ]); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); + expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); + expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('1'); }); it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, '100', { from: holder }); + await this.token.transfer(recipient, NFT1, { from: holder }); + await this.token.transfer(recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const [ t1, t2, t3 ] = await batchInBlock([ () => this.token.delegate(other1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, NFT1, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, NFT2, { from: recipient, gas: 100000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); - // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - const t4 = await this.token.transfer(recipient, 20, { from: holder }); + const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); }); @@ -438,8 +445,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('4'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); }); it('returns zero if < first checkpoint block', async function () { @@ -449,39 +456,41 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder });//TODO: Make it NFT like - await time.advanceBlock(); + const total = await this.token.balanceOf(holder); + + const t1 = await this.token.delegate(other1, { from: holder }); await time.advanceBlock(); - const t2 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + const t2 = await this.token.transfer(other2, NFT1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.transfer(other2, 20, { from: holder }); + const t3 = await this.token.transfer(other2, NFT2, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 30, { from: other2 }); + const t4 = await this.token.transfer(holder, NFT2, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); - + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal(total); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(total); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); }); }); }); describe('getPastTotalSupply', function () { beforeEach(async function () { - await this.token.delegate(holder, { from: holder }); + // await this.token.delegate(holder, { from: holder }); }); it('reverts if block number >= current block', async function () { @@ -501,8 +510,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('returns zero if < first checkpoint block', async function () { @@ -512,7 +521,7 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { @@ -532,7 +541,6 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - console.log(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); From 1675655d826a7aa25e2b67d7b71e152463542308 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:23:31 -0400 Subject: [PATCH 005/300] Updating ERC721Vote tests descriptions --- test/token/ERC721/extensions/ERC721Votes.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index b2d56ea7ab0..49592f36fc8 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -99,7 +99,7 @@ contract('ERC721Votes', function (accounts) { describe('set delegation', function () { describe('call', function () { - it('delegation with balance', async function () {//TODO: Make it NFT like + it('delegation with tokenId', async function () { await this.token.mint(holder, supply); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); @@ -123,7 +123,7 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); }); - it('delegation without balance', async function () { + it('delegation without tokenId', async function () { expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); From 3fabd21e677f839d2f633f263b8226dcf71093f6 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:55:45 -0400 Subject: [PATCH 006/300] Updating ERC721Vote contract and tests --- contracts/token/ERC721/extensions/ERC721Votes.sol | 2 -- test/token/ERC721/extensions/ERC721Votes.test.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 146ff1dd213..978bcd0787e 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -11,8 +11,6 @@ import "../../../utils/cryptography/ECDSA.sol"; * @dev Extension of ERC721 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 {ERC721VotesComp} 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 {getVotes} and {getPastVotes}. diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 49592f36fc8..9712e69d21e 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -490,7 +490,7 @@ contract('ERC721Votes', function (accounts) { describe('getPastTotalSupply', function () { beforeEach(async function () { - // await this.token.delegate(holder, { from: holder }); + await this.token.delegate(holder, { from: holder }); }); it('reverts if block number >= current block', async function () { From e9fde2173cd9010af68ae6c213b50944459b2457 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 14:54:43 -0400 Subject: [PATCH 007/300] Finished tests --- test/token/ERC721/extensions/ERC721Votes.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 9712e69d21e..6d705c7c80f 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -415,16 +415,16 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const [ t1, t2, t3 ] = await batchInBlock([ - () => this.token.delegate(other1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, NFT1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, NFT2, { from: recipient, gas: 100000 }), + () => this.token.delegate(other1, { from: recipient, gas: 200000 }), + () => this.token.transfer(other2, NFT1, { from: recipient, gas: 200000 }), + () => this.token.transfer(other2, NFT2, { from: recipient, gas: 200000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); }); From 975fcd846ff37889deb523b9c021b97d473477cc Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 16:24:20 -0400 Subject: [PATCH 008/300] Adding _afterTokenTransfer to base ERC271 contract --- .../extensions/GovernorVotesERC721.sol | 4 +- contracts/token/ERC721/ERC721.sol | 22 ++++ .../token/ERC721/extensions/ERC721Votes.sol | 19 +-- .../ERC721/extensions/ERC721Votes.test.js | 34 ++--- .../extensions/draft-ERC721Permit.test.js | 117 ------------------ 5 files changed, 44 insertions(+), 152 deletions(-) delete mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 9a2a4ac0ee8..383960d9ef1 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC72GovernorVotesERC721.sol) +// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC721GovernorVotesERC721.sol) pragma solidity ^0.8.0; @@ -12,7 +12,7 @@ import "../../utils/math/Math.sol"; * * _Available since v4.3._ */ -abstract contract ERC72GovernorVotesERC721 is Governor { +abstract contract ERC721GovernorVotesERC721 is Governor { ERC721Votes public immutable token; constructor(ERC721Votes tokenAddress) { diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index dbd91bcbcb8..1e9a88a55db 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -342,6 +342,8 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { _owners[tokenId] = to; emit Transfer(from, to, tokenId); + + _afterTokenTransfer(from, to, tokenId); } /** @@ -421,4 +423,24 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { address to, uint256 tokenId ) internal virtual {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `tokenId` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `tokenId` tokens have been minted for `to`. + * - when `to` is zero, `tokenId` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual {} } diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 978bcd0787e..18164dc6721 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; -import "./draft-ERC721Permit.sol"; +import "../ERC721.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; @@ -22,7 +22,7 @@ import "../../../utils/cryptography/ECDSA.sol"; * * _Available since v4.2._ */ -abstract contract ERC721Votes is ERC721Permit { +abstract contract ERC721Votes is ERC721 { struct Checkpoint { uint32 fromBlock; uint224 votes; @@ -191,7 +191,7 @@ abstract contract ERC721Votes is ERC721Permit { function _afterTokenTransfer( address from, address to - ) internal virtual { + ) internal virtual override{ _moveVotingPower(delegates(from), delegates(to), 1); } @@ -251,17 +251,4 @@ abstract contract ERC721Votes is ERC721Permit { function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } - - /** - * @dev Moves token from the caller's account to `recipient`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 tokenId) external returns (bool){ - _transfer(_msgSender(), recipient, tokenId); - _afterTokenTransfer(_msgSender(), recipient); - return true; - } } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 6d705c7c80f..1a114c1c6b2 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -299,7 +299,7 @@ contract('ERC721Votes', function (accounts) { }); it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); @@ -310,7 +310,7 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); @@ -324,7 +324,7 @@ contract('ERC721Votes', function (accounts) { it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -339,7 +339,7 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -381,20 +381,20 @@ contract('ERC721Votes', function (accounts) { describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { - await this.token.transfer(recipient, NFT1, { from: holder }); //give an account two tokens for readability - await this.token.transfer(recipient, NFT2, { from: holder }); + await this.token.transferFrom(recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transferFrom(recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - const t2 = await this.token.transfer(other2, NFT1, { from: recipient }); + const t2 = await this.token.transferFrom(other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - const t3 = await this.token.transfer(other2, NFT2, { from: recipient }); + const t3 = await this.token.transferFrom(other2, NFT2, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); @@ -410,19 +410,19 @@ contract('ERC721Votes', function (accounts) { }); it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, NFT1, { from: holder }); - await this.token.transfer(recipient, NFT2, { from: holder }); + await this.token.transferFrom(recipient, NFT1, { from: holder }); + await this.token.transferFrom(recipient, NFT2, { 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, NFT1, { from: recipient, gas: 200000 }), - () => this.token.transfer(other2, NFT2, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(other2, NFT1, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(other2, NFT2, { from: recipient, gas: 200000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); - const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); @@ -465,13 +465,13 @@ contract('ERC721Votes', function (accounts) { const t1 = await this.token.delegate(other1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t2 = await this.token.transfer(other2, NFT1, { from: holder }); + const t2 = await this.token.transferFrom(other2, NFT1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.transfer(other2, NFT2, { from: holder }); + const t3 = await this.token.transferFrom(other2, NFT2, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transfer(holder, NFT2, { from: other2 }); + const t4 = await this.token.transferFrom(holder, NFT2, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js deleted file mode 100644 index f22c904c724..00000000000 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable */ - -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); -const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; - -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; - -const ERC721PermitMock = artifacts.require('ERC721PermitMock'); - -const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); - -const Permit = [ - { name: 'owner', type: 'address' }, - { name: 'spender', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint256' }, -]; - -contract('ERC721Permit', function (accounts) { - const [ initialHolder, spender, recipient, other ] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - - const initialTokenId = new BN('100'); - - beforeEach(async function () { - this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); - - // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id - // from within the EVM as from the JSON RPC interface. - // See https://github.com/trufflesuite/ganache-core/issues/515 - this.chainId = await this.token.getChainId(); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect( - await this.token.DOMAIN_SEPARATOR(), - ).to.equal( - await domainSeparator(name, version, this.chainId, this.token.address), - ); - }); - - describe('permit', function () { - const wallet = Wallet.generate(); - - const owner = wallet.getAddressString(); - const value = initialTokenId; - const nonce = 0; - const maxDeadline = MAX_UINT256; - - const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ - primaryType: 'Permit', - types: { EIP712Domain, Permit }, - domain: { name, version, chainId, verifyingContract }, - message: { owner, spender, value, nonce, deadline }, - }); - - it('accepts owner signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); - expect(await this.token.getApproved(value)).to.be.equal(spender); - }); - - it('rejects reused signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects other signature', async function () { - const otherWallet = Wallet.generate(); - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects expired permit', async function () { - const deadline = (await time.latest()) - time.duration.weeks(1); - - const data = buildData(this.chainId, this.token.address, deadline); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, deadline, v, r, s), - 'ERC721Permit: expired deadline', - ); - }); - }); -}); From d996b2ea702289b9863b5ec90ad63222f39ea520 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 16:30:31 -0400 Subject: [PATCH 009/300] Renaming supplied tokenId on tests --- .../ERC721/extensions/draft-ERC721Permit.sol | 87 ------------------- .../ERC721/extensions/draft-IERC721Permit.sol | 60 ------------- .../ERC721/extensions/ERC721Votes.test.js | 34 ++++---- 3 files changed, 17 insertions(+), 164 deletions(-) delete mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol delete mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol deleted file mode 100644 index c00d15367ab..00000000000 --- a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) - -pragma solidity ^0.8.0; - -import "./draft-IERC721Permit.sol"; -import "./ERC721Enumerable.sol"; -import "../../../utils/cryptography/draft-EIP712.sol"; -import "../../../utils/cryptography/ECDSA.sol"; -import "../../../utils/Counters.sol"; - -/** - * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - * - * _Available since v3.4._ - */ -abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { - using Counters for Counters.Counter; - - mapping(address => Counters.Counter) private _nonces; - - // solhint-disable-next-line var-name-mixedcase - bytes32 private immutable _PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - - /** - * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. - * - * It's a good idea to use the same `name` that is defined as the ERC721 token name. - */ - constructor(string memory name) EIP712(name, "1") {} - - /** - * @dev See {IERC721Permit-permit}. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual override { - require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); - - bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); - - bytes32 hash = _hashTypedDataV4(structHash); - - address signer = ECDSA.recover(hash, v, r, s); - require(signer == owner, "ERC721Permit: invalid signature"); - - _approve(spender, value); - } - - /** - * @dev See {IERC721Permit-nonces}. - */ - function nonces(address owner) public view virtual override returns (uint256) { - return _nonces[owner].current(); - } - - /** - * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view override returns (bytes32) { - return _domainSeparatorV4(); - } - - /** - * @dev "Consume a nonce": return the current value and increment. - * - * _Available since v4.1._ - */ - function _useNonce(address owner) internal virtual returns (uint256 current) { - Counters.Counter storage nonce = _nonces[owner]; - current = nonce.current(); - nonce.increment(); - } -} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol deleted file mode 100644 index 61882f2de0d..00000000000 --- a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - */ -interface IERC721Permit { - /** - * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, - * given ``owner``'s signed approval. - * - * IMPORTANT: The same issues {IERC721-approve} has related to transaction - * ordering also apply here. - * - * Emits an {Approval} event. - * - * Requirements: - * - * - `spender` cannot be the zero address. - * - `deadline` must be a timestamp in the future. - * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` - * over the EIP712-formatted function arguments. - * - the signature must use ``owner``'s current nonce (see {nonces}). - * - * For more information on the signature format, see the - * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP - * section]. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - /** - * @dev Returns the current nonce for `owner`. This value must be - * included whenever a signature is generated for {permit}. - * - * Every successful call to {permit} increases ``owner``'s nonce by one. This - * prevents a signature from being used multiple times. - */ - function nonces(address owner) external view returns (uint256); - - /** - * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view returns (bytes32); -} diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 1a114c1c6b2..21d5587d66f 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -61,7 +61,7 @@ contract('ERC721Votes', function (accounts) { const name = 'My Token'; const symbol = 'MTKN'; const version = '1'; - const supply = new BN('10000000000000000000000000'); + const initalTokenId = new BN('10000000000000000000000000'); beforeEach(async function () { this.token = await ERC721VotesMock.new(name, symbol); @@ -89,7 +89,7 @@ contract('ERC721Votes', function (accounts) { this.token.mint(holder, NFT1); this.token.mint(holder, NFT2); this.token.mint(holder, NFT3); - this.token.mint(holder, supply); + this.token.mint(holder, initalTokenId); await expectRevert( this.token.mint(holder, lastTokenId), @@ -100,7 +100,7 @@ contract('ERC721Votes', function (accounts) { describe('set delegation', function () { describe('call', function () { it('delegation with tokenId', async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, initalTokenId); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -151,7 +151,7 @@ contract('ERC721Votes', function (accounts) { }}); beforeEach(async function () { - await this.token.mint(delegatorAddress, supply); + await this.token.mint(delegatorAddress, initalTokenId); }); it('accept signed delegation', async function () { @@ -257,7 +257,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, initalTokenId); await this.token.delegate(holder, { from: holder }); }); @@ -295,12 +295,12 @@ contract('ERC721Votes', function (accounts) { describe('transfers', function () { beforeEach(async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, initalTokenId); }); it('no delegation', async function () { - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -310,8 +310,8 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -324,8 +324,8 @@ contract('ERC721Votes', function (accounts) { it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -339,8 +339,8 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -366,7 +366,7 @@ contract('ERC721Votes', function (accounts) { // 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); + await this.token.mint(holder, initalTokenId); await this.token.mint(holder, NFT1); await this.token.mint(holder, NFT2); await this.token.mint(holder, NFT3); @@ -505,7 +505,7 @@ contract('ERC721Votes', function (accounts) { }); it('returns the latest block if >= last checkpoint block', async function () { - t1 = await this.token.mint(holder, supply); + t1 = await this.token.mint(holder, initalTokenId); await time.advanceBlock(); await time.advanceBlock(); @@ -516,7 +516,7 @@ contract('ERC721Votes', function (accounts) { it('returns zero if < first checkpoint block', async function () { await time.advanceBlock(); - const t1 = await this.token.mint(holder, supply); + const t1 = await this.token.mint(holder, initalTokenId); await time.advanceBlock(); await time.advanceBlock(); From 13a2c044c62169fbdf34ff4a6c7710ed2f969d77 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 17:45:23 -0400 Subject: [PATCH 010/300] Updating tests based on new contract changes --- contracts/mocks/ERC721PermitMock.sol | 20 -------- contracts/mocks/ERC721VotesMock.sol | 2 +- .../token/ERC721/extensions/ERC721Votes.sol | 50 +++++++++++++++++-- .../ERC721/extensions/ERC721Votes.test.js | 34 ++++++------- 4 files changed, 63 insertions(+), 43 deletions(-) delete mode 100644 contracts/mocks/ERC721PermitMock.sol diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol deleted file mode 100644 index 37a860ef4dc..00000000000 --- a/contracts/mocks/ERC721PermitMock.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../token/ERC721/extensions/draft-ERC721Permit.sol"; - -contract ERC721PermitMock is ERC721Permit { - constructor( - string memory name, - string memory symbol, - address initialAccount, - uint256 tokenId - ) payable ERC721(name, symbol) ERC721Permit(name) { - _mint(initialAccount, tokenId); - } - - function getChainId() external view returns (uint256) { - return block.chainid; - } -} diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 09e40a4ea85..3b7a1bed7a8 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { - constructor(string memory name, string memory symbol) ERC721(name, symbol) ERC721Permit(name) {} + constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {} function mint(address account, uint256 tokenId) public { _mint(account, tokenId); diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 18164dc6721..00a5e8707c0 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -4,9 +4,11 @@ pragma solidity ^0.8.0; import "../ERC721.sol"; +import "../../../utils/Counters.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; /** * @dev Extension of ERC721 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. @@ -22,19 +24,29 @@ import "../../../utils/cryptography/ECDSA.sol"; * * _Available since v4.2._ */ -abstract contract ERC721Votes is ERC721 { +abstract contract ERC721Votes is ERC721, EIP712 { + using Counters for Counters.Counter; + struct Checkpoint { uint32 fromBlock; uint224 votes; } - + uint256 _totalSupply; bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); mapping(address => address) private _delegates; + mapping(address => Counters.Counter) private _nonces; mapping(address => Checkpoint[]) private _checkpoints; Checkpoint[] private _totalSupplyCheckpoints; + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC721 token name. + + constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ + /** * @dev Emitted when an account changes their delegate. */ @@ -169,7 +181,8 @@ abstract contract ERC721Votes is ERC721 { */ function _mint(address account, uint256 tokenId) internal virtual override { super._mint(account, tokenId); - require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + _totalSupply += 1; + require(_totalSupply <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } @@ -179,7 +192,7 @@ abstract contract ERC721Votes is ERC721 { */ function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); - + _totalSupply -= 1; _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); } @@ -190,7 +203,8 @@ abstract contract ERC721Votes is ERC721 { */ function _afterTokenTransfer( address from, - address to + address to, + uint256 tokenId ) internal virtual override{ _moveVotingPower(delegates(from), delegates(to), 1); } @@ -244,6 +258,32 @@ abstract contract ERC721Votes is ERC721 { } } + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } + + /** + * @dev Returns an address nonce. + */ + function nonces(address owner) public view virtual returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev Returns DOMAIN_SEPARATOR. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparatorV4(); + } + function _add(uint256 a, uint256 b) private pure returns (uint256) { return a + b; } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 21d5587d66f..a5b9cd16198 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -299,7 +299,7 @@ contract('ERC721Votes', function (accounts) { }); it('no delegation', async function () { - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); @@ -310,7 +310,7 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); @@ -324,7 +324,7 @@ contract('ERC721Votes', function (accounts) { it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -339,7 +339,7 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -381,20 +381,20 @@ contract('ERC721Votes', function (accounts) { describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { - await this.token.transferFrom(recipient, NFT1, { from: holder }); //give an account two tokens for readability - await this.token.transferFrom(recipient, NFT2, { from: holder }); + await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - const t2 = await this.token.transferFrom(other2, NFT1, { from: recipient }); + const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - const t3 = await this.token.transferFrom(other2, NFT2, { from: recipient }); + const t3 = await this.token.transferFrom(recipient, other2, NFT2, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); @@ -410,19 +410,19 @@ contract('ERC721Votes', function (accounts) { }); it('does not add more than one checkpoint in a block', async function () { - await this.token.transferFrom(recipient, NFT1, { from: holder }); - await this.token.transferFrom(recipient, NFT2, { from: holder }); + await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); + await this.token.transferFrom(holder, recipient, NFT2, { 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.transferFrom(other2, NFT1, { from: recipient, gas: 200000 }), - () => this.token.transferFrom(other2, NFT2, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(recipient, other2, NFT1, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(recipient, other2, NFT2, { from: recipient, gas: 200000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); - const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); @@ -465,13 +465,13 @@ contract('ERC721Votes', function (accounts) { const t1 = await this.token.delegate(other1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t2 = await this.token.transferFrom(other2, NFT1, { from: holder }); + const t2 = await this.token.transferFrom(holder, other2, NFT1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.transferFrom(other2, NFT2, { from: holder }); + const t3 = await this.token.transferFrom(holder, other2, NFT2, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transferFrom(holder, NFT2, { from: other2 }); + const t4 = await this.token.transferFrom(other2, holder, NFT2, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); From a7920ec9aa57ca1b94bd1b1a65002fc721f9ac93 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 20:15:54 -0400 Subject: [PATCH 011/300] Updating execution order inside of mint --- contracts/token/ERC721/extensions/ERC721Votes.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 00a5e8707c0..7871282643b 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -180,10 +180,11 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been increased. */ function _mint(address account, uint256 tokenId) internal virtual override { + require(_totalSupply+1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + super._mint(account, tokenId); _totalSupply += 1; - require(_totalSupply <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } From 1d4f6d35070f2d60dc0ae6d166dfdc71f15d9792 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 16:25:14 -0400 Subject: [PATCH 012/300] Adding Mocks for testing and integrating nft minting to current workflow test --- .../extensions/GovernorVotesERC721.sol | 2 +- contracts/mocks/GovernorERC721Mock.sol | 41 +++++++++ test/governance/GovernorWorkflow.behavior.js | 4 +- .../extensions/GovernorERC721.test.js | 91 +++++++++++++++++++ 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 contracts/mocks/GovernorERC721Mock.sol create mode 100644 test/governance/extensions/GovernorERC721.test.js diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 383960d9ef1..2e3079fc243 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -12,7 +12,7 @@ import "../../utils/math/Math.sol"; * * _Available since v4.3._ */ -abstract contract ERC721GovernorVotesERC721 is Governor { +abstract contract GovernorVotesERC721 is Governor { ERC721Votes public immutable token; constructor(ERC721Votes tokenAddress) { diff --git a/contracts/mocks/GovernorERC721Mock.sol b/contracts/mocks/GovernorERC721Mock.sol new file mode 100644 index 00000000000..7508f168334 --- /dev/null +++ b/contracts/mocks/GovernorERC721Mock.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../governance/extensions/GovernorCountingSimple.sol"; +import "../governance/extensions/GovernorVotesERC721.sol"; + +contract GovernorERC721Mock is GovernorVotesERC721, GovernorCountingSimple { + constructor(string memory name_, ERC721Votes token_) Governor(name_) GovernorVotesERC721(token_) {} + + 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; + } + + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 salt + ) public returns (uint256 proposalId) { + return _cancel(targets, values, calldatas, salt); + } + + function getVotes(address account, uint256 blockNumber) + public + view + virtual + override(IGovernor, GovernorVotesERC721) + returns (uint256) + { + return super.getVotes(account, blockNumber); + } +} diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js index 70319cd44d3..e8e2416b19e 100644 --- a/test/governance/GovernorWorkflow.behavior.js +++ b/test/governance/GovernorWorkflow.behavior.js @@ -31,7 +31,9 @@ function runGovernorWorkflow () { for (const voter of this.settings.voters) { if (voter.weight) { await this.token.transfer(voter.voter, voter.weight, { from: this.settings.tokenHolder }); - } + }else if(voter.nftWeight){ + await this.token.transferFrom(this.settings.tokenHolder, voter.voter, voter.nftWeight, { from: this.settings.tokenHolder }); + } } } diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js new file mode 100644 index 00000000000..845d5c20d21 --- /dev/null +++ b/test/governance/extensions/GovernorERC721.test.js @@ -0,0 +1,91 @@ +const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const Enums = require('../../helpers/enums'); + +const { + runGovernorWorkflow, +} = require('./../GovernorWorkflow.behavior'); + +const Token = artifacts.require('ERC721VotesMock'); +const Governor = artifacts.require('GovernorERC721Mock'); +const CallReceiver = artifacts.require('CallReceiverMock'); + +contract('GovernorERC721Mock', function (accounts) { + const [ owner, voter1, voter2, voter3, voter4 ] = accounts; + + const name = 'OZ-Governor'; + const tokenName = 'MockNFToken'; + const tokenSymbol = 'MTKN'; + const initalTokenId = web3.utils.toWei('100'); + const NFT1 = web3.utils.toWei('10'); + const NFT2 = web3.utils.toWei('20'); + const NFT3 = web3.utils.toWei('30'); + + beforeEach(async function () { + this.owner = owner; + this.token = await Token.new(tokenName, tokenSymbol); + this.mock = await Governor.new(name, this.token.address); + this.receiver = await CallReceiver.new(); + await this.token.mint(owner, initalTokenId); + await this.token.mint(owner, NFT1); + await this.token.mint(owner, NFT2); + await this.token.mint(owner, NFT3); + + await this.token.delegate(voter1, { from: voter1 }); + await this.token.delegate(voter2, { from: voter2 }); + await this.token.delegate(voter3, { from: voter3 }); + await this.token.delegate(voter4, { from: voter4 }); + }); + + 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('4'); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16'); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + }); + + describe.only('voting with ERC721 token', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, nftWeight: initalTokenId, support: Enums.VoteType.For }, + { voter: voter2, nftWeight: NFT1, support: Enums.VoteType.For }, + { voter: voter3, nftWeight: NFT2, support: Enums.VoteType.Against }, + { voter: voter4, nftWeight: NFT3, support: Enums.VoteType.Abstain }, + ] + } + }); + + afterEach(async function () { + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter3)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter4)).to.be.equal(true); + + this.receipts.castVote.filter(Boolean).forEach(vote => { + const { voter } = vote.logs.find(Boolean).args; + expectEvent( + vote, + 'VoteCast', + this.settings.voters.find(({ address }) => address === voter), + ); + }); + await this.mock.proposalVotes(this.id).then(result => { + for (const [key, value] of Object.entries(Enums.VoteType)) { + expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( + Object.values(this.settings.voters).filter(({ support }) => support === value).length.toString() + ); + } + }); + }); + runGovernorWorkflow(); + }); +}); From 51f80b0688cf3ea3d67080d0f08499dd6e938975 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 17:02:56 -0400 Subject: [PATCH 013/300] Implementing override test --- .../extensions/GovernorERC721.test.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 845d5c20d21..4f81055800a 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -64,20 +64,22 @@ contract('GovernorERC721Mock', function (accounts) { }); afterEach(async function () { - expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(true); - expect(await this.mock.hasVoted(this.id, voter3)).to.be.equal(true); - expect(await this.mock.hasVoted(this.id, voter4)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); - this.receipts.castVote.filter(Boolean).forEach(vote => { + for(const vote of this.receipts.castVote.filter(Boolean)){ const { voter } = vote.logs.find(Boolean).args; + + expect(await this.mock.hasVoted(this.id, voter)).to.be.equal(true); + expectEvent( vote, 'VoteCast', this.settings.voters.find(({ address }) => address === voter), ); - }); + + expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); + } + await this.mock.proposalVotes(this.id).then(result => { for (const [key, value] of Object.entries(Enums.VoteType)) { expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( @@ -86,6 +88,8 @@ contract('GovernorERC721Mock', function (accounts) { } }); }); + runGovernorWorkflow(); + }); }); From ed0813f1d99515c724f8b0594ed7ac070729c496 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 17:47:26 -0400 Subject: [PATCH 014/300] Removing .only from tests --- test/governance/extensions/GovernorERC721.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 4f81055800a..ee8ad8399da 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -44,7 +44,7 @@ contract('GovernorERC721Mock', function (accounts) { expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); }); - describe.only('voting with ERC721 token', function () { + describe('voting with ERC721 token', function () { beforeEach(async function () { this.settings = { proposal: [ From ee1f36e5673464eaf3f81b89755357554418248b Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 18:05:24 -0400 Subject: [PATCH 015/300] Updating contracts READMEs --- contracts/governance/README.adoc | 4 ++++ contracts/token/ERC721/README.adoc | 2 ++ 2 files changed, 6 insertions(+) diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index d198c9f9315..fc148cfc990 100644 --- a/contracts/governance/README.adoc +++ b/contracts/governance/README.adoc @@ -22,6 +22,8 @@ Votes modules determine the source of voting power, and sometimes quorum number. * {GovernorVotes}: Extracts voting weight from an {ERC20Votes} token. +* {GovernorVotesERC721}: Extracts voting weight from 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. @@ -64,6 +66,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you {{GovernorVotesQuorumFraction}} +{{GovernorVotesERC721}} + {{GovernorVotesComp}} === Extensions diff --git a/contracts/token/ERC721/README.adoc b/contracts/token/ERC721/README.adoc index f1122c53a99..51089e1627c 100644 --- a/contracts/token/ERC721/README.adoc +++ b/contracts/token/ERC721/README.adoc @@ -41,6 +41,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel {{ERC721URIStorage}} +{{ERC721Votes}} + == Presets These contracts are preconfigured combinations of the above features. They can be used through inheritance or as models to copy and paste their source code. From 71af7df63112983841016a23d9f212035edb632d Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 18:57:08 -0400 Subject: [PATCH 016/300] Governance adocs update --- contracts/mocks/ERC721VotesMock.sol | 2 +- contracts/mocks/UserTOkenerc721Mock.sol | 32 +++++++++++++ .../token/ERC721/extensions/ERC721Votes.sol | 3 +- docs/modules/ROOT/pages/erc721.adoc | 2 +- docs/modules/ROOT/pages/governance.adoc | 45 ++++++++++++++++++- 5 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 3b7a1bed7a8..bde65e5a5ff 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { - constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {} + constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} function mint(address account, uint256 tokenId) public { _mint(account, tokenId); diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol new file mode 100644 index 00000000000..b3874e66da5 --- /dev/null +++ b/contracts/mocks/UserTOkenerc721Mock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; + +contract MyToken is ERC721, EIP712, ERC721Votes { + constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._burn(account, amount); + } +} \ No newline at end of file diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 7871282643b..39c03934028 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -44,8 +44,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. * * It's a good idea to use the same `name` that is defined as the ERC721 token name. - - constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ + */ /** * @dev Emitted when an account changes their delegate. diff --git a/docs/modules/ROOT/pages/erc721.adoc b/docs/modules/ROOT/pages/erc721.adoc index 8d28fad2e6e..14dbdc97606 100644 --- a/docs/modules/ROOT/pages/erc721.adoc +++ b/docs/modules/ROOT/pages/erc721.adoc @@ -1,6 +1,6 @@ = ERC721 -We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate* or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. +We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate*, *voting rights*, or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. ERC721 is a more complex standard than ERC20, with multiple optional extensions, and is split across a number of contracts. The OpenZeppelin Contracts provide flexibility regarding how these are combined, along with custom useful extensions. Check out the xref:api:token/ERC721.adoc[API Reference] to learn more about these. diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index c8a26f2a8f2..f041eddd74b 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -14,6 +14,10 @@ This governance protocol is generally implemented in a special-purpose contract OpenZeppelin’s Governor system was designed with a concern for compatibility with existing systems that were based on Compound’s GovernorAlpha and GovernorBravo. Because of this, you will find that many modules are presented in two variants, one of which is built for compatibility with those systems. +=== ERC721Votes + +The ERC721 extension to keep track of votes and vote delegation is one such case. + === ERC20Votes & ERC20VotesComp The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. @@ -119,11 +123,48 @@ contract MyToken is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper { } ``` +If your project requires The voting power of each account in our governance setup will be determined by an ERC721 token. The token has to implement the ERC721Votes extension. This extension will keep track of historical balances so that voting power is retrieved from past snapshots rather than current balance, which is an important protection that prevents double voting. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; + +contract MyToken is ERC721, EIP712, ERC721Votes { + constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._burn(account, amount); + } +} +``` + NOTE: Voting power could be determined in different ways: multiple ERC20 tokens, ERC721 tokens, sybil resistant identities, etc. All of these options are potentially supported by writing a custom Votes module for your Governor. === Governor -Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, and 3) what options people have when casting a vote and how those votes are counted. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. +Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, 3) what options people have when casting a vote and how those votes are counted, and 4)what type of token should be use to vote. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. For 1) we will use the GovernorVotes module, which hooks to an ERC20Votes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. @@ -131,6 +172,8 @@ For 2) we will use GovernorVotesQuorumFraction which works together with ERC20Vo For 3) we will use GovernorCountingSimple, a module that offers 3 options to voters: For, Against, and Abstain, and where only For and Abstain votes are counted towards quorum. +For 4) we will use the GovernorVotesERC721 module, which hooks to an ERC721Votes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. + Besides these modules, Governor itself has some parameters we must set. votingDelay: How long after a proposal is created should voting power be fixed. A large voting delay gives users time to unstake tokens if necessary. From bcd0b03a50ea91e20bb7825f916592ce21eaa196 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sat, 30 Oct 2021 20:49:29 -0400 Subject: [PATCH 017/300] Initial contracts creation --- .../extensions/GovernorVotesERC721.sol | 28 ++ .../token/ERC721/extensions/ERC721Votes.sol | 258 ++++++++++++++++++ .../ERC721/extensions/draft-ERC721Permit.sol | 87 ++++++ .../ERC721/extensions/draft-IERC721Permit.sol | 60 ++++ 4 files changed, 433 insertions(+) create mode 100644 contracts/governance/extensions/GovernorVotesERC721.sol create mode 100644 contracts/token/ERC721/extensions/ERC721Votes.sol create mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol create mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol new file mode 100644 index 00000000000..9a2a4ac0ee8 --- /dev/null +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC72GovernorVotesERC721.sol) + +pragma solidity ^0.8.0; + +import "../Governor.sol"; +import "../../token/ERC721/extensions/ERC721Votes.sol"; +import "../../utils/math/Math.sol"; + +/** + * @dev Extension of {Governor} for voting weight extraction from an {ERC721Votes} token. + * + * _Available since v4.3._ + */ +abstract contract ERC72GovernorVotesERC721 is Governor { + ERC721Votes public immutable token; + + constructor(ERC721Votes tokenAddress) { + token = tokenAddress; + } + + /** + * Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). + */ + function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { + return token.getPastVotes(account, blockNumber); + } +} diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol new file mode 100644 index 00000000000..20da35f32a6 --- /dev/null +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/ERC721Votes.sol) + +pragma solidity ^0.8.0; + +import "./draft-ERC721Permit.sol"; +import "../../../utils/math/Math.sol"; +import "../../../utils/math/SafeCast.sol"; +import "../../../utils/cryptography/ECDSA.sol"; + +/** + * @dev Extension of ERC721 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 {ERC721VotesComp} 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 {getVotes} and {getPastVotes}. + * + * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it + * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. + * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this + * will significantly increase the base gas cost of transfers. + * + * _Available since v4.2._ + */ +abstract contract ERC721Votes is ERC721Permit { + struct Checkpoint { + uint32 fromBlock; + uint224 votes; + } + + bytes32 private constant _DELEGATION_TYPEHASH = + keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + + mapping(address => address) private _delegates; + mapping(address => Checkpoint[]) private _checkpoints; + Checkpoint[] private _totalSupplyCheckpoints; + + /** + * @dev Emitted when an account changes their delegate. + */ + event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); + + /** + * @dev Emitted when a token transfer or delegate change results in changes to an account's voting power. + */ + event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); + + /** + * @dev Get the `pos`-th checkpoint for `account`. + */ + function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoint memory) { + return _checkpoints[account][pos]; + } + + /** + * @dev Get number of checkpoints for `account`. + */ + function numCheckpoints(address account) public view virtual returns (uint32) { + return SafeCast.toUint32(_checkpoints[account].length); + } + + /** + * @dev Get the address `account` is currently delegating to. + */ + function delegates(address account) public view virtual returns (address) { + return _delegates[account]; + } + + /** + * @dev Gets the current votes balance for `account` + */ + function getVotes(address account) public view returns (uint256) { + uint256 pos = _checkpoints[account].length; + return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes; + } + + /** + * @dev Retrieve the number of votes for `account` at the end of `blockNumber`. + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) { + require(blockNumber < block.number, "ERC721Votes: block not yet mined"); + return _checkpointsLookup(_checkpoints[account], blockNumber); + } + + /** + * @dev Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances. + * It is but NOT the sum of all the delegated votes! + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastTotalSupply(uint256 blockNumber) public view returns (uint256) { + require(blockNumber < block.number, "ERC721Votes: block not yet mined"); + return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber); + } + + /** + * @dev Lookup a value in a list of (sorted) checkpoints. + */ + function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) private view returns (uint256) { + // We run a binary search to look for the earliest checkpoint taken after `blockNumber`. + // + // During the loop, the index of the wanted checkpoint remains in the range [low-1, high). + // With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant. + // - If the middle checkpoint is after `blockNumber`, we look in [low, mid) + // - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high) + // Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not + // out of bounds (in which case we're looking too far in the past and the result is 0). + // Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is + // past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out + // the same. + uint256 high = ckpts.length; + uint256 low = 0; + while (low < high) { + uint256 mid = Math.average(low, high); + if (ckpts[mid].fromBlock > blockNumber) { + high = mid; + } else { + low = mid + 1; + } + } + + return high == 0 ? 0 : ckpts[high - 1].votes; + } + + /** + * @dev Delegate votes from the sender to `delegatee`. + */ + function delegate(address delegatee) public virtual { + _delegate(_msgSender(), delegatee); + } + + /** + * @dev Delegates votes from signer to `delegatee` + */ + function delegateBySig( + address delegatee, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + require(block.timestamp <= expiry, "ERC721Votes: signature expired"); + address signer = ECDSA.recover( + _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), + v, + r, + s + ); + require(nonce == _useNonce(signer), "ERC721Votes: invalid nonce"); + _delegate(signer, delegatee); + } + + /** + * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). + */ + function _maxSupply() internal view virtual returns (uint224) { + return type(uint224).max; + } + + /** + * @dev Snapshots the totalSupply after it has been increased. + */ + function _mint(address account, uint256 amount) internal virtual override { + super._mint(account, amount);//TODO: update for NFT + require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + + _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); + } + + /** + * @dev Snapshots the totalSupply after it has been decreased. + */ + function _burn(uint256 tokenId) internal virtual override { + super._burn(tokenId); + + _writeCheckpoint(_totalSupplyCheckpoints, _subtract, tokenId); + } + + /** + * @dev Move voting power when tokens are transferred. + * + * Emits a {DelegateVotesChanged} event. + */ + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual { + _moveVotingPower(delegates(from), delegates(to), amount);//TODO: Update to be NFT logic + } + + /** + * @dev Change delegation for `delegator` to `delegatee`. + * + * Emits events {DelegateChanged} and {DelegateVotesChanged}. + */ + function _delegate(address delegator, address delegatee) internal virtual { + address currentDelegate = delegates(delegator); + uint256 delegatorBalance = balanceOf(delegator); + _delegates[delegator] = delegatee; + + emit DelegateChanged(delegator, currentDelegate, delegatee); + + _moveVotingPower(currentDelegate, delegatee, delegatorBalance); + } + + function _moveVotingPower( + address src, + address dst, + uint256 amount + ) private { + if (src != dst && amount > 0) { + if (src != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[src], _subtract, amount); + emit DelegateVotesChanged(src, oldWeight, newWeight); + } + + if (dst != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[dst], _add, amount); + emit DelegateVotesChanged(dst, oldWeight, newWeight); + } + } + } + + function _writeCheckpoint(//TODO: update for NFT + Checkpoint[] storage ckpts, + function(uint256, uint256) view returns (uint256) op, + uint256 delta + ) private returns (uint256 oldWeight, uint256 newWeight) { + uint256 pos = ckpts.length; + oldWeight = pos == 0 ? 0 : ckpts[pos - 1].votes; + newWeight = op(oldWeight, delta); + + if (pos > 0 && ckpts[pos - 1].fromBlock == block.number) { + ckpts[pos - 1].votes = SafeCast.toUint224(newWeight); + } else { + ckpts.push(Checkpoint({fromBlock: SafeCast.toUint32(block.number), votes: SafeCast.toUint224(newWeight)})); + } + } + + function _add(uint256 a, uint256 b) private pure returns (uint256) { + return a + b; + } + + function _subtract(uint256 a, uint256 b) private pure returns (uint256) { + return a - b; + } +} diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol new file mode 100644 index 00000000000..c00d15367ab --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) + +pragma solidity ^0.8.0; + +import "./draft-IERC721Permit.sol"; +import "./ERC721Enumerable.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; +import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/Counters.sol"; + +/** + * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * _Available since v3.4._ + */ +abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { + using Counters for Counters.Counter; + + mapping(address => Counters.Counter) private _nonces; + + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC721 token name. + */ + constructor(string memory name) EIP712(name, "1") {} + + /** + * @dev See {IERC721Permit-permit}. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); + + bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + + bytes32 hash = _hashTypedDataV4(structHash); + + address signer = ECDSA.recover(hash, v, r, s); + require(signer == owner, "ERC721Permit: invalid signature"); + + _approve(spender, value); + } + + /** + * @dev See {IERC721Permit-nonces}. + */ + function nonces(address owner) public view virtual override returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } +} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol new file mode 100644 index 00000000000..61882f2de0d --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + */ +interface IERC721Permit { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC721-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} From 4c05016389ee1558c1f491a467aca3aec8dc5865 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 09:28:35 -0400 Subject: [PATCH 018/300] creating permit tests --- contracts/mocks/ERC721PermitMock.sol | 20 + contracts/mocks/ERC721VotesMock.sol | 21 + .../ERC721/extensions/ERC721Votes.test.js | 538 ++++++++++++++++++ .../extensions/draft-ERC721Permit.test.js | 117 ++++ 4 files changed, 696 insertions(+) create mode 100644 contracts/mocks/ERC721PermitMock.sol create mode 100644 contracts/mocks/ERC721VotesMock.sol create mode 100644 test/token/ERC721/extensions/ERC721Votes.test.js create mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol new file mode 100644 index 00000000000..37a860ef4dc --- /dev/null +++ b/contracts/mocks/ERC721PermitMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC721/extensions/draft-ERC721Permit.sol"; + +contract ERC721PermitMock is ERC721Permit { + constructor( + string memory name, + string memory symbol, + address initialAccount, + uint256 tokenId + ) payable ERC721(name, symbol) ERC721Permit(name) { + _mint(initialAccount, tokenId); + } + + function getChainId() external view returns (uint256) { + return block.chainid; + } +} diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol new file mode 100644 index 00000000000..457d05eedb9 --- /dev/null +++ b/contracts/mocks/ERC721VotesMock.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC721/extensions/ERC721Votes.sol"; + +contract ERC721VotesMock is ERC721Votes { + constructor(string memory name, string memory symbol) ERC721(name, symbol) ERC721Permit(name) {} + + function mint(address account, uint256 tokenId) public { + _mint(account, tokenId); + } + + function burn(uint256 tokenId) public { + _burn(tokenId); + } + + function getChainId() external view returns (uint256) { + return block.chainid; + } +} diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js new file mode 100644 index 00000000000..7078828039d --- /dev/null +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -0,0 +1,538 @@ +/* eslint-disable */ + +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const { promisify } = require('util'); +const queue = promisify(setImmediate); + +const ERC721VotesMock = artifacts.require('ERC721VotesMock'); + +const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); + +const Delegation = [ + { name: 'delegatee', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'expiry', type: 'uint256' }, +]; + +async function countPendingTransactions() { + return parseInt( + await network.provider.send('eth_getBlockTransactionCountByNumber', ['pending']) + ); +} + +async function batchInBlock (txs) { + try { + // disable auto-mining + await network.provider.send('evm_setAutomine', [false]); + // send all transactions + const promises = txs.map(fn => fn()); + // wait for node to have all pending transactions + while (txs.length > await countPendingTransactions()) { + await queue(); + } + // mine one block + await network.provider.send('evm_mine'); + // fetch receipts + const receipts = await Promise.all(promises); + // Sanity check, all tx should be in the same block + const minedBlocks = new Set(receipts.map(({ receipt }) => receipt.blockNumber)); + expect(minedBlocks.size).to.equal(1); + + return receipts; + } finally { + // enable auto-mining + await network.provider.send('evm_setAutomine', [true]); + } +} + +contract('ERC721Votes', function (accounts) { + const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; + const supply = new BN('10000000000000000000000000'); + + beforeEach(async function () { + this.token = await ERC721VotesMock.new(name, symbol); + + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = await this.token.getChainId(); + }); + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect( + await this.token.DOMAIN_SEPARATOR(), + ).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address), + ); + }); + + it('minting restriction', async function () { + const amount = new BN('2').pow(new BN('224')); + await expectRevert( + this.token.mint(holder, amount), + 'ERC721Votes: total supply risks overflowing votes', + ); + }); + + describe('set delegation', function () { + describe('call', function () { + it('delegation with balance', async function () { + await this.token.mint(holder, supply); + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + + expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); + }); + + it('delegation without balance', async function () { + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + }); + }); + + describe('with signature', function () { + const delegator = Wallet.generate(); + const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); + const nonce = 0; + + const buildData = (chainId, verifyingContract, message) => ({ data: { + primaryType: 'Delegation', + types: { EIP712Domain, Delegation }, + domain: { name, version, chainId, verifyingContract }, + message, + }}); + + beforeEach(async function () { + await this.token.mint(delegatorAddress, supply); + }); + + it('accept signed delegation', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + expectEvent(receipt, 'DelegateChanged', { + delegator: delegatorAddress, + fromDelegate: ZERO_ADDRESS, + toDelegate: delegatorAddress, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: delegatorAddress, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); + + expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); + }); + + it('rejects reused signature', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), + 'ERC721Votes: invalid nonce', + ); + }); + + it('rejects bad delegatee', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + const { logs } = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); + const { args } = logs.find(({ event }) => event == 'DelegateChanged'); + expect(args.delegator).to.not.be.equal(delegatorAddress); + expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); + expect(args.toDelegate).to.be.equal(holderDelegatee); + }); + + it('rejects bad nonce', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), + 'ERC721Votes: invalid nonce', + ); + }); + + it('rejects expired permit', async function () { + const expiry = (await time.latest()) - time.duration.weeks(1); + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry, + }), + )); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), + 'ERC721Votes: signature expired', + ); + }); + }); + }); + + describe('change delegation', function () { + beforeEach(async function () { + await this.token.mint(holder, supply); + await this.token.delegate(holder, { from: holder }); + }); + + it('call', async function () { + expect(await this.token.delegates(holder)).to.be.equal(holder); + + const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: holder, + toDelegate: holderDelegatee, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: supply, + newBalance: '0', + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holderDelegatee, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); + + expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); + expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); + }); + }); + + 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.getVotes(holder)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); + + // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" + const blockNumber = await time.latestBlock(); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getPastVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes); + }); + }); + + // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js. + describe('Compound test suite', function () { + beforeEach(async function () { + await this.token.mint(holder, supply); + }); + + describe('balanceOf', function () { + it('grants to initial account', async function () { + expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); + }); + }); + + describe('numCheckpoints', function () { + it('returns the number of checkpoints for a delegate', async function () { + await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const t1 = await this.token.delegate(other1, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + + const t2 = await this.token.transfer(other2, 10, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + + const t3 = await this.token.transfer(other2, 10, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); + + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '100' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '90' ]); + expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '80' ]); + expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + + await time.advanceBlock(); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); + }); + + it('does not add more than one checkpoint in a block', async function () { + await this.token.transfer(recipient, '100', { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const [ t1, t2, t3 ] = await batchInBlock([ + () => this.token.delegate(other1, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + ]); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); + // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check + // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + }); + }); + + describe('getPastVotes', function () { + it('reverts if block number >= current block', async function () { + await expectRevert( + this.token.getPastVotes(other1, 5e10), + 'ERC721Votes: block not yet mined', + ); + }); + + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.transfer(holder, 20, { from: other2 }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + }); + }); + + describe('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), + 'ERC721Votes: block not yet mined', + ); + }); + + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + t1 = await this.token.mint(holder, supply); + + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const t1 = await this.token.mint(holder, supply); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.mint(holder, supply); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.burn(10); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.burn(10); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.mint(holder, 20); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + }); +}); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js new file mode 100644 index 00000000000..7a7b8cf5d83 --- /dev/null +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -0,0 +1,117 @@ +/* eslint-disable */ + +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const ERC721PermitMock = artifacts.require('ERC721PermitMock'); + +const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); + +const Permit = [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, +]; + +contract('ERC721Permit', function (accounts) { + const [ initialHolder, spender, recipient, other ] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; + + const initialTokenId = new BN('100'); + + beforeEach(async function () { + this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); + + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = await this.token.getChainId(); + }); + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect( + await this.token.DOMAIN_SEPARATOR(), + ).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address), + ); + }); + + describe.only('permit', function () { + const wallet = Wallet.generate(); + + const owner = wallet.getAddressString(); + const value = initialTokenId; + const nonce = 0; + const maxDeadline = MAX_UINT256; + + const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ + primaryType: 'Permit', + types: { EIP712Domain, Permit }, + domain: { name, version, chainId, verifyingContract }, + message: { owner, spender, value, nonce, deadline }, + }); + + it('accepts owner signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); + expect(await this.token.getApproved(value)).to.be.equal(spender); + }); + + it('rejects reused signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects other signature', async function () { + const otherWallet = Wallet.generate(); + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects expired permit', async function () { + const deadline = (await time.latest()) - time.duration.weeks(1); + + const data = buildData(this.chainId, this.token.address, deadline); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, deadline, v, r, s), + 'ERC721Permit: expired deadline', + ); + }); + }); +}); From 0dd29da4095bb5de707d26f5c084184cffcaaa3f Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 10:26:22 -0400 Subject: [PATCH 019/300] Fixing checkpoints count --- .../token/ERC721/extensions/ERC721Votes.sol | 11 ++--- .../ERC721/extensions/ERC721Votes.test.js | 47 ++++++++++++------- .../extensions/draft-ERC721Permit.test.js | 2 +- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 20da35f32a6..b184c9c83fa 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -7,7 +7,6 @@ import "./draft-ERC721Permit.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; - /** * @dev Extension of ERC721 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. @@ -170,11 +169,11 @@ abstract contract ERC721Votes is ERC721Permit { /** * @dev Snapshots the totalSupply after it has been increased. */ - function _mint(address account, uint256 amount) internal virtual override { - super._mint(account, amount);//TODO: update for NFT + function _mint(address account, uint256 tokenId) internal virtual override { + super._mint(account, tokenId); require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } /** @@ -183,7 +182,7 @@ abstract contract ERC721Votes is ERC721Permit { function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); - _writeCheckpoint(_totalSupplyCheckpoints, _subtract, tokenId); + _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); } /** @@ -232,7 +231,7 @@ abstract contract ERC721Votes is ERC721Permit { } } - function _writeCheckpoint(//TODO: update for NFT + function _writeCheckpoint( Checkpoint[] storage ckpts, function(uint256, uint256) view returns (uint256) op, uint256 delta diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 7078828039d..d2981dd5f9d 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -54,7 +54,9 @@ async function batchInBlock (txs) { contract('ERC721Votes', function (accounts) { const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; - + const NFT1 = new BN('10'); + const NFT2 = new BN('20'); + const NFT3 = new BN('30'); const name = 'My Token'; const symbol = 'MTKN'; const version = '1'; @@ -91,7 +93,7 @@ contract('ERC721Votes', function (accounts) { describe('set delegation', function () { describe('call', function () { - it('delegation with balance', async function () { + it('delegation with balance', async function () {//TODO: Make it NFT like await this.token.mint(holder, supply); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); @@ -249,7 +251,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, supply);//TODO: Avoid tokenId duplicate await this.token.delegate(holder, { from: holder }); }); @@ -359,6 +361,9 @@ contract('ERC721Votes', function (accounts) { describe('Compound test suite', function () { beforeEach(async function () { await this.token.mint(holder, supply); + await this.token.mint(holder, NFT1); + await this.token.mint(holder, NFT2); + await this.token.mint(holder, NFT3); }); describe('balanceOf', function () { @@ -448,16 +453,16 @@ contract('ERC721Votes', function (accounts) { }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); + const t1 = await this.token.delegate(other1, { from: holder });//TODO: Make it NFT like 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 }); + const t3 = await this.token.transfer(other2, 20, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 20, { from: other2 }); + const t4 = await this.token.transfer(holder, 30, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); @@ -511,28 +516,34 @@ contract('ERC721Votes', function (accounts) { }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.mint(holder, supply); + const t1 = await this.token.mint(holder, NFT1); await time.advanceBlock(); await time.advanceBlock(); - const t2 = await this.token.burn(10); + const t2 = await this.token.burn(NFT1); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.burn(10); + const t3 = await this.token.mint(holder, NFT2); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.mint(holder, 20); + const t4 = await this.token.burn(NFT2); + await time.advanceBlock(); + await time.advanceBlock(); + const t5 = await this.token.mint(holder, NFT3); await time.advanceBlock(); await time.advanceBlock(); + console.log(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); }); }); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js index 7a7b8cf5d83..f22c904c724 100644 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -50,7 +50,7 @@ contract('ERC721Permit', function (accounts) { ); }); - describe.only('permit', function () { + describe('permit', function () { const wallet = Wallet.generate(); const owner = wallet.getAddressString(); From 71a6ce18239b62c9f1fd50cee7eb5b98307b0d3b Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:20:14 -0400 Subject: [PATCH 020/300] Updating ERC721Vote tests --- contracts/mocks/ERC721VotesMock.sol | 4 + .../token/ERC721/extensions/ERC721Votes.sol | 18 ++- .../ERC721/extensions/ERC721Votes.test.js | 146 +++++++++--------- 3 files changed, 96 insertions(+), 72 deletions(-) diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 457d05eedb9..09e40a4ea85 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -18,4 +18,8 @@ contract ERC721VotesMock is ERC721Votes { function getChainId() external view returns (uint256) { return block.chainid; } + + function _maxSupply() internal pure override returns(uint224){ + return uint224(4); + } } diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index b184c9c83fa..146ff1dd213 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -192,10 +192,9 @@ abstract contract ERC721Votes is ERC721Permit { */ function _afterTokenTransfer( address from, - address to, - uint256 amount + address to ) internal virtual { - _moveVotingPower(delegates(from), delegates(to), amount);//TODO: Update to be NFT logic + _moveVotingPower(delegates(from), delegates(to), 1); } /** @@ -254,4 +253,17 @@ abstract contract ERC721Votes is ERC721Permit { function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } + + /** + * @dev Moves token from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 tokenId) external returns (bool){ + _transfer(_msgSender(), recipient, tokenId); + _afterTokenTransfer(_msgSender(), recipient); + return true; + } } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index d2981dd5f9d..b2d56ea7ab0 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -14,6 +14,7 @@ const queue = promisify(setImmediate); const ERC721VotesMock = artifacts.require('ERC721VotesMock'); const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); +const { Console } = require('console'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -84,9 +85,14 @@ contract('ERC721Votes', function (accounts) { }); it('minting restriction', async function () { - const amount = new BN('2').pow(new BN('224')); + const lastTokenId = new BN('2').pow(new BN('224')); + this.token.mint(holder, NFT1); + this.token.mint(holder, NFT2); + this.token.mint(holder, NFT3); + this.token.mint(holder, supply); + await expectRevert( - this.token.mint(holder, amount), + this.token.mint(holder, lastTokenId), 'ERC721Votes: total supply risks overflowing votes', ); }); @@ -106,15 +112,15 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '0', - newBalance: supply, + newBalance: '1', }); expect(await this.token.delegates(holder)).to.be.equal(holder); - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); + expect(await this.token.getVotes(holder)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); }); it('delegation without balance', async function () { @@ -169,15 +175,15 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: delegatorAddress, previousBalance: '0', - newBalance: supply, + newBalance: '1', }); expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); - expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); + expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1'); }); it('rejects reused signature', async function () { @@ -251,7 +257,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { - await this.token.mint(holder, supply);//TODO: Avoid tokenId duplicate + await this.token.mint(holder, supply); await this.token.delegate(holder, { from: holder }); }); @@ -266,24 +272,24 @@ contract('ERC721Votes', function (accounts) { }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, - previousBalance: supply, + previousBalance: '1', newBalance: '0', }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holderDelegatee, previousBalance: '0', - newBalance: supply, + newBalance: '1', }); expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); - expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); + expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); }); }); @@ -293,8 +299,8 @@ contract('ERC721Votes', function (accounts) { }); it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -304,22 +310,22 @@ contract('ERC721Votes', function (accounts) { 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 { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); 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.holderVotes = '0'; 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' }); + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -333,15 +339,15 @@ contract('ERC721Votes', function (accounts) { 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) }); + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); 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.holderVotes = '0'; this.recipientVotes = '1'; }); @@ -368,54 +374,55 @@ contract('ERC721Votes', function (accounts) { describe('balanceOf', function () { it('grants to initial account', async function () { - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('4'); }); }); 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 + + await this.token.transfer(recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transfer(recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - - const t2 = await this.token.transfer(other2, 10, { from: recipient }); + + const t2 = await this.token.transfer(other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - - const t3 = await this.token.transfer(other2, 10, { from: recipient }); + + const t3 = await this.token.transfer(other2, NFT2, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - const t4 = await this.token.transfer(recipient, 20, { from: holder }); + const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '100' ]); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '90' ]); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '80' ]); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); + expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); + expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('1'); }); it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, '100', { from: holder }); + await this.token.transfer(recipient, NFT1, { from: holder }); + await this.token.transfer(recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const [ t1, t2, t3 ] = await batchInBlock([ () => this.token.delegate(other1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, NFT1, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, NFT2, { from: recipient, gas: 100000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); - // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - const t4 = await this.token.transfer(recipient, 20, { from: holder }); + const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); }); @@ -438,8 +445,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('4'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); }); it('returns zero if < first checkpoint block', async function () { @@ -449,39 +456,41 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder });//TODO: Make it NFT like - await time.advanceBlock(); + const total = await this.token.balanceOf(holder); + + const t1 = await this.token.delegate(other1, { from: holder }); await time.advanceBlock(); - const t2 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + const t2 = await this.token.transfer(other2, NFT1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.transfer(other2, 20, { from: holder }); + const t3 = await this.token.transfer(other2, NFT2, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 30, { from: other2 }); + const t4 = await this.token.transfer(holder, NFT2, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); - + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal(total); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(total); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); }); }); }); describe('getPastTotalSupply', function () { beforeEach(async function () { - await this.token.delegate(holder, { from: holder }); + // await this.token.delegate(holder, { from: holder }); }); it('reverts if block number >= current block', async function () { @@ -501,8 +510,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('returns zero if < first checkpoint block', async function () { @@ -512,7 +521,7 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { @@ -532,7 +541,6 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - console.log(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); From e6c3337b166765c0266cb08bb60c6afd2f222342 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:23:31 -0400 Subject: [PATCH 021/300] Updating ERC721Vote tests descriptions --- test/token/ERC721/extensions/ERC721Votes.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index b2d56ea7ab0..49592f36fc8 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -99,7 +99,7 @@ contract('ERC721Votes', function (accounts) { describe('set delegation', function () { describe('call', function () { - it('delegation with balance', async function () {//TODO: Make it NFT like + it('delegation with tokenId', async function () { await this.token.mint(holder, supply); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); @@ -123,7 +123,7 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); }); - it('delegation without balance', async function () { + it('delegation without tokenId', async function () { expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); From 18dcf8eb1a355395d9bc7595125a6bd3bbca57e8 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:55:45 -0400 Subject: [PATCH 022/300] Updating ERC721Vote contract and tests --- contracts/token/ERC721/extensions/ERC721Votes.sol | 2 -- test/token/ERC721/extensions/ERC721Votes.test.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 146ff1dd213..978bcd0787e 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -11,8 +11,6 @@ import "../../../utils/cryptography/ECDSA.sol"; * @dev Extension of ERC721 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 {ERC721VotesComp} 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 {getVotes} and {getPastVotes}. diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 49592f36fc8..9712e69d21e 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -490,7 +490,7 @@ contract('ERC721Votes', function (accounts) { describe('getPastTotalSupply', function () { beforeEach(async function () { - // await this.token.delegate(holder, { from: holder }); + await this.token.delegate(holder, { from: holder }); }); it('reverts if block number >= current block', async function () { From 1fd39d29fa7f39953f28d85721206ddb9ebe9cca Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 14:54:43 -0400 Subject: [PATCH 023/300] Finished tests --- test/token/ERC721/extensions/ERC721Votes.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 9712e69d21e..6d705c7c80f 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -415,16 +415,16 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const [ t1, t2, t3 ] = await batchInBlock([ - () => this.token.delegate(other1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, NFT1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, NFT2, { from: recipient, gas: 100000 }), + () => this.token.delegate(other1, { from: recipient, gas: 200000 }), + () => this.token.transfer(other2, NFT1, { from: recipient, gas: 200000 }), + () => this.token.transfer(other2, NFT2, { from: recipient, gas: 200000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); }); From 1f4b751486d42b86ef305b32f26ccc504e8921ed Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 16:24:20 -0400 Subject: [PATCH 024/300] Adding _afterTokenTransfer to base ERC271 contract --- .../extensions/GovernorVotesERC721.sol | 4 +- contracts/token/ERC721/ERC721.sol | 22 ++++ .../token/ERC721/extensions/ERC721Votes.sol | 19 +-- .../ERC721/extensions/ERC721Votes.test.js | 34 ++--- .../extensions/draft-ERC721Permit.test.js | 117 ------------------ 5 files changed, 44 insertions(+), 152 deletions(-) delete mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 9a2a4ac0ee8..383960d9ef1 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC72GovernorVotesERC721.sol) +// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC721GovernorVotesERC721.sol) pragma solidity ^0.8.0; @@ -12,7 +12,7 @@ import "../../utils/math/Math.sol"; * * _Available since v4.3._ */ -abstract contract ERC72GovernorVotesERC721 is Governor { +abstract contract ERC721GovernorVotesERC721 is Governor { ERC721Votes public immutable token; constructor(ERC721Votes tokenAddress) { diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index dbd91bcbcb8..1e9a88a55db 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -342,6 +342,8 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { _owners[tokenId] = to; emit Transfer(from, to, tokenId); + + _afterTokenTransfer(from, to, tokenId); } /** @@ -421,4 +423,24 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { address to, uint256 tokenId ) internal virtual {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `tokenId` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `tokenId` tokens have been minted for `to`. + * - when `to` is zero, `tokenId` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual {} } diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 978bcd0787e..18164dc6721 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; -import "./draft-ERC721Permit.sol"; +import "../ERC721.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; @@ -22,7 +22,7 @@ import "../../../utils/cryptography/ECDSA.sol"; * * _Available since v4.2._ */ -abstract contract ERC721Votes is ERC721Permit { +abstract contract ERC721Votes is ERC721 { struct Checkpoint { uint32 fromBlock; uint224 votes; @@ -191,7 +191,7 @@ abstract contract ERC721Votes is ERC721Permit { function _afterTokenTransfer( address from, address to - ) internal virtual { + ) internal virtual override{ _moveVotingPower(delegates(from), delegates(to), 1); } @@ -251,17 +251,4 @@ abstract contract ERC721Votes is ERC721Permit { function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } - - /** - * @dev Moves token from the caller's account to `recipient`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 tokenId) external returns (bool){ - _transfer(_msgSender(), recipient, tokenId); - _afterTokenTransfer(_msgSender(), recipient); - return true; - } } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 6d705c7c80f..1a114c1c6b2 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -299,7 +299,7 @@ contract('ERC721Votes', function (accounts) { }); it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); @@ -310,7 +310,7 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); @@ -324,7 +324,7 @@ contract('ERC721Votes', function (accounts) { it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -339,7 +339,7 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -381,20 +381,20 @@ contract('ERC721Votes', function (accounts) { describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { - await this.token.transfer(recipient, NFT1, { from: holder }); //give an account two tokens for readability - await this.token.transfer(recipient, NFT2, { from: holder }); + await this.token.transferFrom(recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transferFrom(recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - const t2 = await this.token.transfer(other2, NFT1, { from: recipient }); + const t2 = await this.token.transferFrom(other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - const t3 = await this.token.transfer(other2, NFT2, { from: recipient }); + const t3 = await this.token.transferFrom(other2, NFT2, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); @@ -410,19 +410,19 @@ contract('ERC721Votes', function (accounts) { }); it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, NFT1, { from: holder }); - await this.token.transfer(recipient, NFT2, { from: holder }); + await this.token.transferFrom(recipient, NFT1, { from: holder }); + await this.token.transferFrom(recipient, NFT2, { 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, NFT1, { from: recipient, gas: 200000 }), - () => this.token.transfer(other2, NFT2, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(other2, NFT1, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(other2, NFT2, { from: recipient, gas: 200000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); - const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); @@ -465,13 +465,13 @@ contract('ERC721Votes', function (accounts) { const t1 = await this.token.delegate(other1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t2 = await this.token.transfer(other2, NFT1, { from: holder }); + const t2 = await this.token.transferFrom(other2, NFT1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.transfer(other2, NFT2, { from: holder }); + const t3 = await this.token.transferFrom(other2, NFT2, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transfer(holder, NFT2, { from: other2 }); + const t4 = await this.token.transferFrom(holder, NFT2, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js deleted file mode 100644 index f22c904c724..00000000000 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable */ - -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); -const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; - -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; - -const ERC721PermitMock = artifacts.require('ERC721PermitMock'); - -const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); - -const Permit = [ - { name: 'owner', type: 'address' }, - { name: 'spender', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint256' }, -]; - -contract('ERC721Permit', function (accounts) { - const [ initialHolder, spender, recipient, other ] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - - const initialTokenId = new BN('100'); - - beforeEach(async function () { - this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); - - // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id - // from within the EVM as from the JSON RPC interface. - // See https://github.com/trufflesuite/ganache-core/issues/515 - this.chainId = await this.token.getChainId(); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect( - await this.token.DOMAIN_SEPARATOR(), - ).to.equal( - await domainSeparator(name, version, this.chainId, this.token.address), - ); - }); - - describe('permit', function () { - const wallet = Wallet.generate(); - - const owner = wallet.getAddressString(); - const value = initialTokenId; - const nonce = 0; - const maxDeadline = MAX_UINT256; - - const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ - primaryType: 'Permit', - types: { EIP712Domain, Permit }, - domain: { name, version, chainId, verifyingContract }, - message: { owner, spender, value, nonce, deadline }, - }); - - it('accepts owner signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); - expect(await this.token.getApproved(value)).to.be.equal(spender); - }); - - it('rejects reused signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects other signature', async function () { - const otherWallet = Wallet.generate(); - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects expired permit', async function () { - const deadline = (await time.latest()) - time.duration.weeks(1); - - const data = buildData(this.chainId, this.token.address, deadline); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, deadline, v, r, s), - 'ERC721Permit: expired deadline', - ); - }); - }); -}); From fd61c46823e779c8abed264a867a6cb2f851268c Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 16:30:31 -0400 Subject: [PATCH 025/300] Renaming supplied tokenId on tests --- .../ERC721/extensions/draft-ERC721Permit.sol | 87 ------------------- .../ERC721/extensions/draft-IERC721Permit.sol | 60 ------------- .../ERC721/extensions/ERC721Votes.test.js | 34 ++++---- 3 files changed, 17 insertions(+), 164 deletions(-) delete mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol delete mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol deleted file mode 100644 index c00d15367ab..00000000000 --- a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) - -pragma solidity ^0.8.0; - -import "./draft-IERC721Permit.sol"; -import "./ERC721Enumerable.sol"; -import "../../../utils/cryptography/draft-EIP712.sol"; -import "../../../utils/cryptography/ECDSA.sol"; -import "../../../utils/Counters.sol"; - -/** - * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - * - * _Available since v3.4._ - */ -abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { - using Counters for Counters.Counter; - - mapping(address => Counters.Counter) private _nonces; - - // solhint-disable-next-line var-name-mixedcase - bytes32 private immutable _PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - - /** - * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. - * - * It's a good idea to use the same `name` that is defined as the ERC721 token name. - */ - constructor(string memory name) EIP712(name, "1") {} - - /** - * @dev See {IERC721Permit-permit}. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual override { - require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); - - bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); - - bytes32 hash = _hashTypedDataV4(structHash); - - address signer = ECDSA.recover(hash, v, r, s); - require(signer == owner, "ERC721Permit: invalid signature"); - - _approve(spender, value); - } - - /** - * @dev See {IERC721Permit-nonces}. - */ - function nonces(address owner) public view virtual override returns (uint256) { - return _nonces[owner].current(); - } - - /** - * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view override returns (bytes32) { - return _domainSeparatorV4(); - } - - /** - * @dev "Consume a nonce": return the current value and increment. - * - * _Available since v4.1._ - */ - function _useNonce(address owner) internal virtual returns (uint256 current) { - Counters.Counter storage nonce = _nonces[owner]; - current = nonce.current(); - nonce.increment(); - } -} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol deleted file mode 100644 index 61882f2de0d..00000000000 --- a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - */ -interface IERC721Permit { - /** - * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, - * given ``owner``'s signed approval. - * - * IMPORTANT: The same issues {IERC721-approve} has related to transaction - * ordering also apply here. - * - * Emits an {Approval} event. - * - * Requirements: - * - * - `spender` cannot be the zero address. - * - `deadline` must be a timestamp in the future. - * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` - * over the EIP712-formatted function arguments. - * - the signature must use ``owner``'s current nonce (see {nonces}). - * - * For more information on the signature format, see the - * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP - * section]. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - /** - * @dev Returns the current nonce for `owner`. This value must be - * included whenever a signature is generated for {permit}. - * - * Every successful call to {permit} increases ``owner``'s nonce by one. This - * prevents a signature from being used multiple times. - */ - function nonces(address owner) external view returns (uint256); - - /** - * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view returns (bytes32); -} diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 1a114c1c6b2..21d5587d66f 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -61,7 +61,7 @@ contract('ERC721Votes', function (accounts) { const name = 'My Token'; const symbol = 'MTKN'; const version = '1'; - const supply = new BN('10000000000000000000000000'); + const initalTokenId = new BN('10000000000000000000000000'); beforeEach(async function () { this.token = await ERC721VotesMock.new(name, symbol); @@ -89,7 +89,7 @@ contract('ERC721Votes', function (accounts) { this.token.mint(holder, NFT1); this.token.mint(holder, NFT2); this.token.mint(holder, NFT3); - this.token.mint(holder, supply); + this.token.mint(holder, initalTokenId); await expectRevert( this.token.mint(holder, lastTokenId), @@ -100,7 +100,7 @@ contract('ERC721Votes', function (accounts) { describe('set delegation', function () { describe('call', function () { it('delegation with tokenId', async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, initalTokenId); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -151,7 +151,7 @@ contract('ERC721Votes', function (accounts) { }}); beforeEach(async function () { - await this.token.mint(delegatorAddress, supply); + await this.token.mint(delegatorAddress, initalTokenId); }); it('accept signed delegation', async function () { @@ -257,7 +257,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, initalTokenId); await this.token.delegate(holder, { from: holder }); }); @@ -295,12 +295,12 @@ contract('ERC721Votes', function (accounts) { describe('transfers', function () { beforeEach(async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, initalTokenId); }); it('no delegation', async function () { - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -310,8 +310,8 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -324,8 +324,8 @@ contract('ERC721Votes', function (accounts) { it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -339,8 +339,8 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -366,7 +366,7 @@ contract('ERC721Votes', function (accounts) { // 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); + await this.token.mint(holder, initalTokenId); await this.token.mint(holder, NFT1); await this.token.mint(holder, NFT2); await this.token.mint(holder, NFT3); @@ -505,7 +505,7 @@ contract('ERC721Votes', function (accounts) { }); it('returns the latest block if >= last checkpoint block', async function () { - t1 = await this.token.mint(holder, supply); + t1 = await this.token.mint(holder, initalTokenId); await time.advanceBlock(); await time.advanceBlock(); @@ -516,7 +516,7 @@ contract('ERC721Votes', function (accounts) { it('returns zero if < first checkpoint block', async function () { await time.advanceBlock(); - const t1 = await this.token.mint(holder, supply); + const t1 = await this.token.mint(holder, initalTokenId); await time.advanceBlock(); await time.advanceBlock(); From 94b17dd00d471f9c78292d626c6d1c60ce252504 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 17:45:23 -0400 Subject: [PATCH 026/300] Updating tests based on new contract changes --- contracts/mocks/ERC721PermitMock.sol | 20 -------- contracts/mocks/ERC721VotesMock.sol | 2 +- .../token/ERC721/extensions/ERC721Votes.sol | 50 +++++++++++++++++-- .../ERC721/extensions/ERC721Votes.test.js | 34 ++++++------- 4 files changed, 63 insertions(+), 43 deletions(-) delete mode 100644 contracts/mocks/ERC721PermitMock.sol diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol deleted file mode 100644 index 37a860ef4dc..00000000000 --- a/contracts/mocks/ERC721PermitMock.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../token/ERC721/extensions/draft-ERC721Permit.sol"; - -contract ERC721PermitMock is ERC721Permit { - constructor( - string memory name, - string memory symbol, - address initialAccount, - uint256 tokenId - ) payable ERC721(name, symbol) ERC721Permit(name) { - _mint(initialAccount, tokenId); - } - - function getChainId() external view returns (uint256) { - return block.chainid; - } -} diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 09e40a4ea85..3b7a1bed7a8 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { - constructor(string memory name, string memory symbol) ERC721(name, symbol) ERC721Permit(name) {} + constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {} function mint(address account, uint256 tokenId) public { _mint(account, tokenId); diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 18164dc6721..00a5e8707c0 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -4,9 +4,11 @@ pragma solidity ^0.8.0; import "../ERC721.sol"; +import "../../../utils/Counters.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; /** * @dev Extension of ERC721 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. @@ -22,19 +24,29 @@ import "../../../utils/cryptography/ECDSA.sol"; * * _Available since v4.2._ */ -abstract contract ERC721Votes is ERC721 { +abstract contract ERC721Votes is ERC721, EIP712 { + using Counters for Counters.Counter; + struct Checkpoint { uint32 fromBlock; uint224 votes; } - + uint256 _totalSupply; bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); mapping(address => address) private _delegates; + mapping(address => Counters.Counter) private _nonces; mapping(address => Checkpoint[]) private _checkpoints; Checkpoint[] private _totalSupplyCheckpoints; + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC721 token name. + + constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ + /** * @dev Emitted when an account changes their delegate. */ @@ -169,7 +181,8 @@ abstract contract ERC721Votes is ERC721 { */ function _mint(address account, uint256 tokenId) internal virtual override { super._mint(account, tokenId); - require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + _totalSupply += 1; + require(_totalSupply <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } @@ -179,7 +192,7 @@ abstract contract ERC721Votes is ERC721 { */ function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); - + _totalSupply -= 1; _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); } @@ -190,7 +203,8 @@ abstract contract ERC721Votes is ERC721 { */ function _afterTokenTransfer( address from, - address to + address to, + uint256 tokenId ) internal virtual override{ _moveVotingPower(delegates(from), delegates(to), 1); } @@ -244,6 +258,32 @@ abstract contract ERC721Votes is ERC721 { } } + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } + + /** + * @dev Returns an address nonce. + */ + function nonces(address owner) public view virtual returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev Returns DOMAIN_SEPARATOR. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparatorV4(); + } + function _add(uint256 a, uint256 b) private pure returns (uint256) { return a + b; } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 21d5587d66f..a5b9cd16198 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -299,7 +299,7 @@ contract('ERC721Votes', function (accounts) { }); it('no delegation', async function () { - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); @@ -310,7 +310,7 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); @@ -324,7 +324,7 @@ contract('ERC721Votes', function (accounts) { it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -339,7 +339,7 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -381,20 +381,20 @@ contract('ERC721Votes', function (accounts) { describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { - await this.token.transferFrom(recipient, NFT1, { from: holder }); //give an account two tokens for readability - await this.token.transferFrom(recipient, NFT2, { from: holder }); + await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - const t2 = await this.token.transferFrom(other2, NFT1, { from: recipient }); + const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - const t3 = await this.token.transferFrom(other2, NFT2, { from: recipient }); + const t3 = await this.token.transferFrom(recipient, other2, NFT2, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); @@ -410,19 +410,19 @@ contract('ERC721Votes', function (accounts) { }); it('does not add more than one checkpoint in a block', async function () { - await this.token.transferFrom(recipient, NFT1, { from: holder }); - await this.token.transferFrom(recipient, NFT2, { from: holder }); + await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); + await this.token.transferFrom(holder, recipient, NFT2, { 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.transferFrom(other2, NFT1, { from: recipient, gas: 200000 }), - () => this.token.transferFrom(other2, NFT2, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(recipient, other2, NFT1, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(recipient, other2, NFT2, { from: recipient, gas: 200000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); - const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); @@ -465,13 +465,13 @@ contract('ERC721Votes', function (accounts) { const t1 = await this.token.delegate(other1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t2 = await this.token.transferFrom(other2, NFT1, { from: holder }); + const t2 = await this.token.transferFrom(holder, other2, NFT1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.transferFrom(other2, NFT2, { from: holder }); + const t3 = await this.token.transferFrom(holder, other2, NFT2, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transferFrom(holder, NFT2, { from: other2 }); + const t4 = await this.token.transferFrom(other2, holder, NFT2, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); From 06d8ebc7c6e3345f73bf2b7e3edbf2e9bcaa2dcd Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 20:15:54 -0400 Subject: [PATCH 027/300] Updating execution order inside of mint --- contracts/token/ERC721/extensions/ERC721Votes.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 00a5e8707c0..7871282643b 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -180,10 +180,11 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been increased. */ function _mint(address account, uint256 tokenId) internal virtual override { + require(_totalSupply+1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + super._mint(account, tokenId); _totalSupply += 1; - require(_totalSupply <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } From eb33a0a1599342e86f4a9c704789dbb6b85bbf5f Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 16:25:14 -0400 Subject: [PATCH 028/300] Adding Mocks for testing and integrating nft minting to current workflow test --- .../extensions/GovernorVotesERC721.sol | 2 +- contracts/mocks/GovernorERC721Mock.sol | 41 +++++++++ test/governance/GovernorWorkflow.behavior.js | 4 +- .../extensions/GovernorERC721.test.js | 91 +++++++++++++++++++ 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 contracts/mocks/GovernorERC721Mock.sol create mode 100644 test/governance/extensions/GovernorERC721.test.js diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 383960d9ef1..2e3079fc243 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -12,7 +12,7 @@ import "../../utils/math/Math.sol"; * * _Available since v4.3._ */ -abstract contract ERC721GovernorVotesERC721 is Governor { +abstract contract GovernorVotesERC721 is Governor { ERC721Votes public immutable token; constructor(ERC721Votes tokenAddress) { diff --git a/contracts/mocks/GovernorERC721Mock.sol b/contracts/mocks/GovernorERC721Mock.sol new file mode 100644 index 00000000000..7508f168334 --- /dev/null +++ b/contracts/mocks/GovernorERC721Mock.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../governance/extensions/GovernorCountingSimple.sol"; +import "../governance/extensions/GovernorVotesERC721.sol"; + +contract GovernorERC721Mock is GovernorVotesERC721, GovernorCountingSimple { + constructor(string memory name_, ERC721Votes token_) Governor(name_) GovernorVotesERC721(token_) {} + + 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; + } + + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 salt + ) public returns (uint256 proposalId) { + return _cancel(targets, values, calldatas, salt); + } + + function getVotes(address account, uint256 blockNumber) + public + view + virtual + override(IGovernor, GovernorVotesERC721) + returns (uint256) + { + return super.getVotes(account, blockNumber); + } +} diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js index 70319cd44d3..e8e2416b19e 100644 --- a/test/governance/GovernorWorkflow.behavior.js +++ b/test/governance/GovernorWorkflow.behavior.js @@ -31,7 +31,9 @@ function runGovernorWorkflow () { for (const voter of this.settings.voters) { if (voter.weight) { await this.token.transfer(voter.voter, voter.weight, { from: this.settings.tokenHolder }); - } + }else if(voter.nftWeight){ + await this.token.transferFrom(this.settings.tokenHolder, voter.voter, voter.nftWeight, { from: this.settings.tokenHolder }); + } } } diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js new file mode 100644 index 00000000000..845d5c20d21 --- /dev/null +++ b/test/governance/extensions/GovernorERC721.test.js @@ -0,0 +1,91 @@ +const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const Enums = require('../../helpers/enums'); + +const { + runGovernorWorkflow, +} = require('./../GovernorWorkflow.behavior'); + +const Token = artifacts.require('ERC721VotesMock'); +const Governor = artifacts.require('GovernorERC721Mock'); +const CallReceiver = artifacts.require('CallReceiverMock'); + +contract('GovernorERC721Mock', function (accounts) { + const [ owner, voter1, voter2, voter3, voter4 ] = accounts; + + const name = 'OZ-Governor'; + const tokenName = 'MockNFToken'; + const tokenSymbol = 'MTKN'; + const initalTokenId = web3.utils.toWei('100'); + const NFT1 = web3.utils.toWei('10'); + const NFT2 = web3.utils.toWei('20'); + const NFT3 = web3.utils.toWei('30'); + + beforeEach(async function () { + this.owner = owner; + this.token = await Token.new(tokenName, tokenSymbol); + this.mock = await Governor.new(name, this.token.address); + this.receiver = await CallReceiver.new(); + await this.token.mint(owner, initalTokenId); + await this.token.mint(owner, NFT1); + await this.token.mint(owner, NFT2); + await this.token.mint(owner, NFT3); + + await this.token.delegate(voter1, { from: voter1 }); + await this.token.delegate(voter2, { from: voter2 }); + await this.token.delegate(voter3, { from: voter3 }); + await this.token.delegate(voter4, { from: voter4 }); + }); + + 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('4'); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16'); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + }); + + describe.only('voting with ERC721 token', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, nftWeight: initalTokenId, support: Enums.VoteType.For }, + { voter: voter2, nftWeight: NFT1, support: Enums.VoteType.For }, + { voter: voter3, nftWeight: NFT2, support: Enums.VoteType.Against }, + { voter: voter4, nftWeight: NFT3, support: Enums.VoteType.Abstain }, + ] + } + }); + + afterEach(async function () { + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter3)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter4)).to.be.equal(true); + + this.receipts.castVote.filter(Boolean).forEach(vote => { + const { voter } = vote.logs.find(Boolean).args; + expectEvent( + vote, + 'VoteCast', + this.settings.voters.find(({ address }) => address === voter), + ); + }); + await this.mock.proposalVotes(this.id).then(result => { + for (const [key, value] of Object.entries(Enums.VoteType)) { + expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( + Object.values(this.settings.voters).filter(({ support }) => support === value).length.toString() + ); + } + }); + }); + runGovernorWorkflow(); + }); +}); From b49c5c077d947803bf88fe73abbd0ec8dc36d7aa Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 17:02:56 -0400 Subject: [PATCH 029/300] Implementing override test --- .../extensions/GovernorERC721.test.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 845d5c20d21..4f81055800a 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -64,20 +64,22 @@ contract('GovernorERC721Mock', function (accounts) { }); afterEach(async function () { - expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(true); - expect(await this.mock.hasVoted(this.id, voter3)).to.be.equal(true); - expect(await this.mock.hasVoted(this.id, voter4)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); - this.receipts.castVote.filter(Boolean).forEach(vote => { + for(const vote of this.receipts.castVote.filter(Boolean)){ const { voter } = vote.logs.find(Boolean).args; + + expect(await this.mock.hasVoted(this.id, voter)).to.be.equal(true); + expectEvent( vote, 'VoteCast', this.settings.voters.find(({ address }) => address === voter), ); - }); + + expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); + } + await this.mock.proposalVotes(this.id).then(result => { for (const [key, value] of Object.entries(Enums.VoteType)) { expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( @@ -86,6 +88,8 @@ contract('GovernorERC721Mock', function (accounts) { } }); }); + runGovernorWorkflow(); + }); }); From 8f18f33e3d96815b3f5f68d5a91a486f9b10e56c Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 17:47:26 -0400 Subject: [PATCH 030/300] Removing .only from tests --- test/governance/extensions/GovernorERC721.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 4f81055800a..ee8ad8399da 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -44,7 +44,7 @@ contract('GovernorERC721Mock', function (accounts) { expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); }); - describe.only('voting with ERC721 token', function () { + describe('voting with ERC721 token', function () { beforeEach(async function () { this.settings = { proposal: [ From b9810408c5c235798ede0047eb9c7c8f134b1ef0 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 18:05:24 -0400 Subject: [PATCH 031/300] Updating contracts READMEs --- contracts/governance/README.adoc | 4 ++++ contracts/token/ERC721/README.adoc | 2 ++ 2 files changed, 6 insertions(+) diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index d198c9f9315..fc148cfc990 100644 --- a/contracts/governance/README.adoc +++ b/contracts/governance/README.adoc @@ -22,6 +22,8 @@ Votes modules determine the source of voting power, and sometimes quorum number. * {GovernorVotes}: Extracts voting weight from an {ERC20Votes} token. +* {GovernorVotesERC721}: Extracts voting weight from 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. @@ -64,6 +66,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you {{GovernorVotesQuorumFraction}} +{{GovernorVotesERC721}} + {{GovernorVotesComp}} === Extensions diff --git a/contracts/token/ERC721/README.adoc b/contracts/token/ERC721/README.adoc index f1122c53a99..51089e1627c 100644 --- a/contracts/token/ERC721/README.adoc +++ b/contracts/token/ERC721/README.adoc @@ -41,6 +41,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel {{ERC721URIStorage}} +{{ERC721Votes}} + == Presets These contracts are preconfigured combinations of the above features. They can be used through inheritance or as models to copy and paste their source code. From 724042b9650083a0860d4dc179e4499ff5d8fb96 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 18:57:08 -0400 Subject: [PATCH 032/300] Governance adocs update --- contracts/mocks/ERC721VotesMock.sol | 2 +- contracts/mocks/UserTOkenerc721Mock.sol | 32 +++++++++++++ .../token/ERC721/extensions/ERC721Votes.sol | 3 +- docs/modules/ROOT/pages/erc721.adoc | 2 +- docs/modules/ROOT/pages/governance.adoc | 45 ++++++++++++++++++- 5 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 3b7a1bed7a8..bde65e5a5ff 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { - constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {} + constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} function mint(address account, uint256 tokenId) public { _mint(account, tokenId); diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol new file mode 100644 index 00000000000..b3874e66da5 --- /dev/null +++ b/contracts/mocks/UserTOkenerc721Mock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; + +contract MyToken is ERC721, EIP712, ERC721Votes { + constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._burn(account, amount); + } +} \ No newline at end of file diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 7871282643b..39c03934028 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -44,8 +44,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. * * It's a good idea to use the same `name` that is defined as the ERC721 token name. - - constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ + */ /** * @dev Emitted when an account changes their delegate. diff --git a/docs/modules/ROOT/pages/erc721.adoc b/docs/modules/ROOT/pages/erc721.adoc index 8d28fad2e6e..14dbdc97606 100644 --- a/docs/modules/ROOT/pages/erc721.adoc +++ b/docs/modules/ROOT/pages/erc721.adoc @@ -1,6 +1,6 @@ = ERC721 -We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate* or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. +We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate*, *voting rights*, or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. ERC721 is a more complex standard than ERC20, with multiple optional extensions, and is split across a number of contracts. The OpenZeppelin Contracts provide flexibility regarding how these are combined, along with custom useful extensions. Check out the xref:api:token/ERC721.adoc[API Reference] to learn more about these. diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 5fcd9e0f378..917ad983b04 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -14,6 +14,10 @@ This governance protocol is generally implemented in a special-purpose contract OpenZeppelin’s Governor system was designed with a concern for compatibility with existing systems that were based on Compound’s GovernorAlpha and GovernorBravo. Because of this, you will find that many modules are presented in two variants, one of which is built for compatibility with those systems. +=== ERC721Votes + +The ERC721 extension to keep track of votes and vote delegation is one such case. + === ERC20Votes & ERC20VotesComp The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. @@ -119,11 +123,48 @@ contract MyToken is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper { } ``` +If your project requires The voting power of each account in our governance setup will be determined by an ERC721 token. The token has to implement the ERC721Votes extension. This extension will keep track of historical balances so that voting power is retrieved from past snapshots rather than current balance, which is an important protection that prevents double voting. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; + +contract MyToken is ERC721, EIP712, ERC721Votes { + constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._burn(account, amount); + } +} +``` + NOTE: Voting power could be determined in different ways: multiple ERC20 tokens, ERC721 tokens, sybil resistant identities, etc. All of these options are potentially supported by writing a custom Votes module for your Governor. === Governor -Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, and 3) what options people have when casting a vote and how those votes are counted. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. +Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, 3) what options people have when casting a vote and how those votes are counted, and 4)what type of token should be use to vote. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. For 1) we will use the GovernorVotes module, which hooks to an ERC20Votes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. @@ -131,6 +172,8 @@ For 2) we will use GovernorVotesQuorumFraction which works together with ERC20Vo For 3) we will use GovernorCountingSimple, a module that offers 3 options to voters: For, Against, and Abstain, and where only For and Abstain votes are counted towards quorum. +For 4) we will use the GovernorVotesERC721 module, which hooks to an ERC721Votes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. + Besides these modules, Governor itself has some parameters we must set. votingDelay: How long after a proposal is created should voting power be fixed. A large voting delay gives users time to unstake tokens if necessary. From 9cbbff7be63943486f78838a48ddcdb86b46e767 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 14:13:44 -0400 Subject: [PATCH 033/300] Removing test contract --- contracts/mocks/UserTOkenerc721Mock.sol | 32 ------------------------- 1 file changed, 32 deletions(-) delete mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol deleted file mode 100644 index b3874e66da5..00000000000 --- a/contracts/mocks/UserTOkenerc721Mock.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "@openzeppelin/contracts/token/ERC721.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; - -contract MyToken is ERC721, EIP712, ERC721Votes { - constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._afterTokenTransfer(from, to, amount); - } - - function _mint(address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._mint(to, amount); - } - - function _burn(address account, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._burn(account, amount); - } -} \ No newline at end of file From 7e9702bf9511c47267966720367db8ea4f6b1dcf Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 14:46:27 -0400 Subject: [PATCH 034/300] Updating contracts listing order --- docs/modules/ROOT/pages/governance.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 917ad983b04..40414667815 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -14,14 +14,14 @@ This governance protocol is generally implemented in a special-purpose contract OpenZeppelin’s Governor system was designed with a concern for compatibility with existing systems that were based on Compound’s GovernorAlpha and GovernorBravo. Because of this, you will find that many modules are presented in two variants, one of which is built for compatibility with those systems. -=== ERC721Votes - -The ERC721 extension to keep track of votes and vote delegation is one such case. - === ERC20Votes & ERC20VotesComp The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. +=== ERC721Votes + +The ERC721 extension to keep track of votes and vote delegation is one such case. + === Governor & GovernorCompatibilityBravo An OpenZeppelin Governor contract is by default not interface-compatible with GovernorAlpha or Bravo, since some of the functions are different or missing, although it shares all of the same events. However, it’s possible to opt in to full compatibility by inheriting from the GovernorCompatibilityBravo module. The contract will be cheaper to deploy and use without this module. From 6dd7987d9f2ae8050d8913a4aa5edba0b668cb40 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 15:02:56 -0400 Subject: [PATCH 035/300] Following lint suggestions --- test/governance/GovernorWorkflow.behavior.js | 7 ++++--- .../extensions/GovernorERC721.test.js | 17 ++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js index e8e2416b19e..ae178d7c9de 100644 --- a/test/governance/GovernorWorkflow.behavior.js +++ b/test/governance/GovernorWorkflow.behavior.js @@ -31,9 +31,10 @@ function runGovernorWorkflow () { for (const voter of this.settings.voters) { if (voter.weight) { await this.token.transfer(voter.voter, voter.weight, { from: this.settings.tokenHolder }); - }else if(voter.nftWeight){ - await this.token.transferFrom(this.settings.tokenHolder, voter.voter, voter.nftWeight, { from: this.settings.tokenHolder }); - } + } else if (voter.nftWeight) { + await this.token.transferFrom(this.settings.tokenHolder, voter.voter, voter.nftWeight, + { from: this.settings.tokenHolder }); + } } } diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index ee8ad8399da..69eb788da0e 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -1,4 +1,4 @@ -const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const Enums = require('../../helpers/enums'); const { @@ -29,7 +29,7 @@ contract('GovernorERC721Mock', function (accounts) { await this.token.mint(owner, NFT1); await this.token.mint(owner, NFT2); await this.token.mint(owner, NFT3); - + await this.token.delegate(voter1, { from: voter1 }); await this.token.delegate(voter2, { from: voter2 }); await this.token.delegate(voter3, { from: voter3 }); @@ -59,16 +59,16 @@ contract('GovernorERC721Mock', function (accounts) { { voter: voter2, nftWeight: NFT1, support: Enums.VoteType.For }, { voter: voter3, nftWeight: NFT2, support: Enums.VoteType.Against }, { voter: voter4, nftWeight: NFT3, support: Enums.VoteType.Abstain }, - ] - } + ], + }; }); afterEach(async function () { - expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); - for(const vote of this.receipts.castVote.filter(Boolean)){ + for (const vote of this.receipts.castVote.filter(Boolean)) { const { voter } = vote.logs.find(Boolean).args; - + expect(await this.mock.hasVoted(this.id, voter)).to.be.equal(true); expectEvent( @@ -83,13 +83,12 @@ contract('GovernorERC721Mock', function (accounts) { await this.mock.proposalVotes(this.id).then(result => { for (const [key, value] of Object.entries(Enums.VoteType)) { expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( - Object.values(this.settings.voters).filter(({ support }) => support === value).length.toString() + Object.values(this.settings.voters).filter(({ support }) => support === value).length.toString(), ); } }); }); runGovernorWorkflow(); - }); }); From e7ff23f9c13782b103eed53f6a09bdc5b72ec0e1 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 15:18:09 -0400 Subject: [PATCH 036/300] Delete of test contract --- contracts/mocks/UserTOkenerc721Mock.sol | 32 ------------------------- 1 file changed, 32 deletions(-) delete mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol deleted file mode 100644 index b3874e66da5..00000000000 --- a/contracts/mocks/UserTOkenerc721Mock.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "@openzeppelin/contracts/token/ERC721.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; - -contract MyToken is ERC721, EIP712, ERC721Votes { - constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._afterTokenTransfer(from, to, amount); - } - - function _mint(address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._mint(to, amount); - } - - function _burn(address account, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._burn(account, amount); - } -} \ No newline at end of file From 37f3e0e49f365f65f5f32bfda3b8e0833f3ad007 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 18:14:44 -0400 Subject: [PATCH 037/300] Updating comment and documentation --- contracts/governance/IGovernor.sol | 2 +- docs/modules/ROOT/pages/governance.adoc | 27 ++++++++++--------------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index b30a2aa0e3b..dd954ef3efa 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -193,7 +193,7 @@ abstract contract IGovernor is IERC165 { function castVote(uint256 proposalId, uint8 support) public virtual returns (uint256 balance); /** - * @dev Cast a with a reason + * @dev Cast a vote with a reason * * Emits a {VoteCast} event. */ diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 40414667815..6aa71a8ed18 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -129,7 +129,7 @@ If your project requires The voting power of each account in our governance setu // SPDX-License-Identifier: MIT pragma solidity ^0.8.2; -import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; contract MyToken is ERC721, EIP712, ERC721Votes { @@ -137,25 +137,20 @@ contract MyToken is ERC721, EIP712, ERC721Votes { // The functions below are overrides required by Solidity. - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._afterTokenTransfer(from, to, amount); + function _afterTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal override(ERC721, ERC721Votes) { + super._afterTokenTransfer(from, to, tokenId); } - function _mint(address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._mint(to, amount); + function _mint(address to, uint256 tokenId) internal override(ERC721, ERC721Votes) { + super._mint(to, tokenId); } - function _burn(address account, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._burn(account, amount); + function _burn(uint256 tokenId) internal override(ERC721, ERC721Votes) { + super._burn(tokenId); } } ``` From b34554dde8c6b5154fa9ba4e94ddd716b401c53b Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 12:45:06 -0400 Subject: [PATCH 038/300] Update docs/modules/ROOT/pages/governance.adoc Co-authored-by: Francisco Giordano --- docs/modules/ROOT/pages/governance.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 6aa71a8ed18..2c7b52e92cb 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -159,7 +159,7 @@ NOTE: Voting power could be determined in different ways: multiple ERC20 tokens, === Governor -Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, 3) what options people have when casting a vote and how those votes are counted, and 4)what type of token should be use to vote. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. +Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, 3) what options people have when casting a vote and how those votes are counted, and 4) what type of token should be used to vote. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. For 1) we will use the GovernorVotes module, which hooks to an ERC20Votes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. From 01cd5300d853f6f62f76f7dc8cbd5cb7edd6e506 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 12:46:21 -0400 Subject: [PATCH 039/300] Update contracts/token/ERC721/extensions/ERC721Votes.sol Co-authored-by: Francisco Giordano --- contracts/token/ERC721/extensions/ERC721Votes.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 39c03934028..b0b8f2ca9d4 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -205,7 +205,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { address from, address to, uint256 tokenId - ) internal virtual override{ + ) internal virtual override { _moveVotingPower(delegates(from), delegates(to), 1); } From f15925d3aea8bc3f116379fcda64d6d4cce60423 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 12:53:42 -0400 Subject: [PATCH 040/300] Update contracts/token/ERC721/extensions/ERC721Votes.sol Co-authored-by: Francisco Giordano --- contracts/token/ERC721/extensions/ERC721Votes.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index b0b8f2ca9d4..2376a9a7b8c 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -179,7 +179,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been increased. */ function _mint(address account, uint256 tokenId) internal virtual override { - require(_totalSupply+1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + require(_totalSupply + 1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); super._mint(account, tokenId); _totalSupply += 1; From 0d93566e82a91436f00d8116a981d9eb72567faf Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 12:54:01 -0400 Subject: [PATCH 041/300] Update docs/modules/ROOT/pages/governance.adoc Co-authored-by: Francisco Giordano --- docs/modules/ROOT/pages/governance.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 2c7b52e92cb..0b8f6e13615 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -167,7 +167,7 @@ For 2) we will use GovernorVotesQuorumFraction which works together with ERC20Vo For 3) we will use GovernorCountingSimple, a module that offers 3 options to voters: For, Against, and Abstain, and where only For and Abstain votes are counted towards quorum. -For 4) we will use the GovernorVotesERC721 module, which hooks to an ERC721Votes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. +For 4) we will use the GovernorVotesERC721 module, which hooks to an ERC721Votes instance to determine the voting power of an account based on the NFTs they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. Besides these modules, Governor itself has some parameters we must set. From 0553ee159de9e2e16a87b3770882730420a64a11 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 12:55:58 -0400 Subject: [PATCH 042/300] Improving documentation, adding spaces, renaming variables --- .../extensions/GovernorVotesERC721.sol | 10 +++-- contracts/mocks/ERC721VotesMock.sol | 2 +- contracts/token/ERC721/ERC721.sol | 5 +-- contracts/token/ERC721/README.adoc | 1 + ...{ERC721Votes.sol => draft-ERC721Votes.sol} | 9 +++-- docs/modules/ROOT/pages/governance.adoc | 2 +- .../ERC721/extensions/ERC721Votes.test.js | 39 +++++++++---------- 7 files changed, 35 insertions(+), 33 deletions(-) rename contracts/token/ERC721/extensions/{ERC721Votes.sol => draft-ERC721Votes.sol} (98%) diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 2e3079fc243..63f62b9775a 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -4,23 +4,25 @@ pragma solidity ^0.8.0; import "../Governor.sol"; -import "../../token/ERC721/extensions/ERC721Votes.sol"; +import "../../token/ERC721/extensions/draft-ERC721Votes.sol"; import "../../utils/math/Math.sol"; /** * @dev Extension of {Governor} for voting weight extraction from an {ERC721Votes} token. * - * _Available since v4.3._ + * _Available since v4.5._ */ abstract contract GovernorVotesERC721 is Governor { ERC721Votes public immutable token; - + /** + * @dev Need the ERC721Votes address to be initialized + */ constructor(ERC721Votes tokenAddress) { token = tokenAddress; } /** - * Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). + * @dev Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). */ function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { return token.getPastVotes(account, blockNumber); diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index bde65e5a5ff..3ba1a9287cb 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; -import "../token/ERC721/extensions/ERC721Votes.sol"; +import "../token/ERC721/extensions/draft-ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 1e9a88a55db..2a6cfcd1d02 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -343,7 +343,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { emit Transfer(from, to, tokenId); - _afterTokenTransfer(from, to, tokenId); + _afterTokenTransfer(from, to); } /** @@ -440,7 +440,6 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { */ function _afterTokenTransfer( address from, - address to, - uint256 tokenId + address to ) internal virtual {} } diff --git a/contracts/token/ERC721/README.adoc b/contracts/token/ERC721/README.adoc index 51089e1627c..90ec73783f0 100644 --- a/contracts/token/ERC721/README.adoc +++ b/contracts/token/ERC721/README.adoc @@ -15,6 +15,7 @@ Additionally there are multiple custom extensions, including: * designation of addresses that can pause token transfers for all users ({ERC721Pausable}). * destruction of own tokens ({ERC721Burnable}). +* support for voting and vote delegation ({ERC721Votes}) NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC721 (such as <>) and expose them as external functions in the way they prefer. On the other hand, xref:ROOT:erc721.adoc#Presets[ERC721 Presets] (such as {ERC721PresetMinterPauserAutoId}) are designed using opinionated patterns to provide developers with ready to use, deployable contracts. diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol similarity index 98% rename from contracts/token/ERC721/extensions/ERC721Votes.sol rename to contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 39c03934028..b6f851c7df7 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/ERC721Votes.sol) +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Votes.sol) pragma solidity ^0.8.0; @@ -22,7 +22,7 @@ import "../../../utils/cryptography/draft-EIP712.sol"; * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this * will significantly increase the base gas cost of transfers. * - * _Available since v4.2._ + * _Available since v4.5._ */ abstract contract ERC721Votes is ERC721, EIP712 { using Counters for Counters.Counter; @@ -203,9 +203,10 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _afterTokenTransfer( address from, - address to, - uint256 tokenId + address to ) internal virtual override{ + super._afterTokenTransfer(from, to); + _moveVotingPower(delegates(from), delegates(to), 1); } diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 2c7b52e92cb..26cb2358ee6 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -130,7 +130,7 @@ If your project requires The voting power of each account in our governance setu pragma solidity ^0.8.2; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/draft-ERC721Votes.sol"; contract MyToken is ERC721, EIP712, ERC721Votes { constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index a5b9cd16198..29e3ba6df81 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -14,7 +14,6 @@ const queue = promisify(setImmediate); const ERC721VotesMock = artifacts.require('ERC721VotesMock'); const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); -const { Console } = require('console'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -61,7 +60,7 @@ contract('ERC721Votes', function (accounts) { const name = 'My Token'; const symbol = 'MTKN'; const version = '1'; - const initalTokenId = new BN('10000000000000000000000000'); + const NFT0 = new BN('10000000000000000000000000'); beforeEach(async function () { this.token = await ERC721VotesMock.new(name, symbol); @@ -89,7 +88,7 @@ contract('ERC721Votes', function (accounts) { this.token.mint(holder, NFT1); this.token.mint(holder, NFT2); this.token.mint(holder, NFT3); - this.token.mint(holder, initalTokenId); + this.token.mint(holder, NFT0); await expectRevert( this.token.mint(holder, lastTokenId), @@ -99,8 +98,8 @@ contract('ERC721Votes', function (accounts) { describe('set delegation', function () { describe('call', function () { - it('delegation with tokenId', async function () { - await this.token.mint(holder, initalTokenId); + it('delegation with tokens', async function () { + await this.token.mint(holder, NFT0); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -123,7 +122,7 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); }); - it('delegation without tokenId', async function () { + it('delegation without tokens', async function () { expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -151,7 +150,7 @@ contract('ERC721Votes', function (accounts) { }}); beforeEach(async function () { - await this.token.mint(delegatorAddress, initalTokenId); + await this.token.mint(delegatorAddress, NFT0); }); it('accept signed delegation', async function () { @@ -257,7 +256,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { - await this.token.mint(holder, initalTokenId); + await this.token.mint(holder, NFT0); await this.token.delegate(holder, { from: holder }); }); @@ -295,12 +294,12 @@ contract('ERC721Votes', function (accounts) { describe('transfers', function () { beforeEach(async function () { - await this.token.mint(holder, initalTokenId); + await this.token.mint(holder, NFT0); }); it('no delegation', async function () { - const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -310,8 +309,8 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); - const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -324,8 +323,8 @@ contract('ERC721Votes', function (accounts) { it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -339,8 +338,8 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -366,7 +365,7 @@ contract('ERC721Votes', function (accounts) { // 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, initalTokenId); + await this.token.mint(holder, NFT0); await this.token.mint(holder, NFT1); await this.token.mint(holder, NFT2); await this.token.mint(holder, NFT3); @@ -505,7 +504,7 @@ contract('ERC721Votes', function (accounts) { }); it('returns the latest block if >= last checkpoint block', async function () { - t1 = await this.token.mint(holder, initalTokenId); + t1 = await this.token.mint(holder, NFT0); await time.advanceBlock(); await time.advanceBlock(); @@ -516,7 +515,7 @@ contract('ERC721Votes', function (accounts) { it('returns zero if < first checkpoint block', async function () { await time.advanceBlock(); - const t1 = await this.token.mint(holder, initalTokenId); + const t1 = await this.token.mint(holder, NFT0); await time.advanceBlock(); await time.advanceBlock(); From 53557b93b9aa5f6f46ff161fb1879c72e9fd2e49 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 13:00:30 -0400 Subject: [PATCH 043/300] Update erc721.adoc "Voting rights" are generally fungible --- docs/modules/ROOT/pages/erc721.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/erc721.adoc b/docs/modules/ROOT/pages/erc721.adoc index 14dbdc97606..8d28fad2e6e 100644 --- a/docs/modules/ROOT/pages/erc721.adoc +++ b/docs/modules/ROOT/pages/erc721.adoc @@ -1,6 +1,6 @@ = ERC721 -We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate*, *voting rights*, or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. +We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate* or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. ERC721 is a more complex standard than ERC20, with multiple optional extensions, and is split across a number of contracts. The OpenZeppelin Contracts provide flexibility regarding how these are combined, along with custom useful extensions. Check out the xref:api:token/ERC721.adoc[API Reference] to learn more about these. From c358a635cca69d86eebdf8d3638c27497ec06600 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 14:13:58 -0400 Subject: [PATCH 044/300] Adding constructor --- contracts/mocks/ERC721VotesMock.sol | 5 ++-- .../ERC721/extensions/draft-ERC721Votes.sol | 3 ++- test/governance/GovernorWorkflow.behavior.js | 7 ++++-- .../extensions/GovernorERC721.test.js | 24 ++++++++++++------- .../ERC721/extensions/ERC721Votes.test.js | 8 ++++--- 5 files changed, 30 insertions(+), 17 deletions(-) diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 3ba1a9287cb..92184352e51 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,7 +5,8 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/draft-ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { - constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} + + constructor(string memory name, string memory symbol) ERC721Votes(name, symbol) { } function mint(address account, uint256 tokenId) public { _mint(account, tokenId); @@ -20,6 +21,6 @@ contract ERC721VotesMock is ERC721Votes { } function _maxSupply() internal pure override returns(uint224){ - return uint224(4); + return uint224(5); } } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 3b6f7e80cf2..30d4500e584 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -45,7 +45,8 @@ abstract contract ERC721Votes is ERC721, EIP712 { * * It's a good idea to use the same `name` that is defined as the ERC721 token name. */ - + constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} + /** * @dev Emitted when an account changes their delegate. */ diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js index ae178d7c9de..0cc1162790f 100644 --- a/test/governance/GovernorWorkflow.behavior.js +++ b/test/governance/GovernorWorkflow.behavior.js @@ -31,9 +31,11 @@ function runGovernorWorkflow () { for (const voter of this.settings.voters) { if (voter.weight) { await this.token.transfer(voter.voter, voter.weight, { from: this.settings.tokenHolder }); - } else if (voter.nftWeight) { - await this.token.transferFrom(this.settings.tokenHolder, voter.voter, voter.nftWeight, + } else if (voter.nfts) { + for (const nft of voter.nfts) { + await this.token.transferFrom(this.settings.tokenHolder, voter.voter, nft, { from: this.settings.tokenHolder }); + } } } } @@ -69,6 +71,7 @@ function runGovernorWorkflow () { if (tryGet(this.settings, 'voters')) { this.receipts.castVote = []; for (const voter of this.settings.voters) { + console.log('voting',voter); if (!voter.signature) { this.receipts.castVote.push( await getReceiptOrRevert( diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 69eb788da0e..4c2efb050f8 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -15,20 +15,22 @@ contract('GovernorERC721Mock', function (accounts) { const name = 'OZ-Governor'; const tokenName = 'MockNFToken'; const tokenSymbol = 'MTKN'; - const initalTokenId = web3.utils.toWei('100'); + const NFT0 = web3.utils.toWei('100'); const NFT1 = web3.utils.toWei('10'); const NFT2 = web3.utils.toWei('20'); const NFT3 = web3.utils.toWei('30'); + const NFT4 = web3.utils.toWei('40'); beforeEach(async function () { this.owner = owner; this.token = await Token.new(tokenName, tokenSymbol); this.mock = await Governor.new(name, this.token.address); this.receiver = await CallReceiver.new(); - await this.token.mint(owner, initalTokenId); + await this.token.mint(owner, NFT0); await this.token.mint(owner, NFT1); await this.token.mint(owner, NFT2); await this.token.mint(owner, NFT3); + await this.token.mint(owner, NFT4); await this.token.delegate(voter1, { from: voter1 }); await this.token.delegate(voter2, { from: voter2 }); @@ -44,7 +46,7 @@ contract('GovernorERC721Mock', function (accounts) { expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); }); - describe('voting with ERC721 token', function () { + describe.only('voting with ERC721 token', function () { beforeEach(async function () { this.settings = { proposal: [ @@ -55,10 +57,10 @@ contract('GovernorERC721Mock', function (accounts) { ], tokenHolder: owner, voters: [ - { voter: voter1, nftWeight: initalTokenId, support: Enums.VoteType.For }, - { voter: voter2, nftWeight: NFT1, support: Enums.VoteType.For }, - { voter: voter3, nftWeight: NFT2, support: Enums.VoteType.Against }, - { voter: voter4, nftWeight: NFT3, support: Enums.VoteType.Abstain }, + { voter: voter1, nfts: [NFT0], support: Enums.VoteType.For }, + { voter: voter2, nfts: [NFT1, NFT2], support: Enums.VoteType.For }, + { voter: voter3, nfts: [NFT3], support: Enums.VoteType.Against }, + { voter: voter4, nfts: [NFT4], support: Enums.VoteType.Abstain }, ], }; }); @@ -76,8 +78,12 @@ contract('GovernorERC721Mock', function (accounts) { 'VoteCast', this.settings.voters.find(({ address }) => address === voter), ); - - expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); + console.log(voter); + if (voter == voter2) { + expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('2'); + } else { + expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); + } } await this.mock.proposalVotes(this.id).then(result => { diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 29e3ba6df81..196af93ec83 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -53,14 +53,15 @@ async function batchInBlock (txs) { } contract('ERC721Votes', function (accounts) { - const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; + const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; + const NFT0 = new BN('10000000000000000000000000'); const NFT1 = new BN('10'); const NFT2 = new BN('20'); - const NFT3 = new BN('30'); + const NFT3 = new BN('30'); + const NFT4 = new BN('40'); const name = 'My Token'; const symbol = 'MTKN'; const version = '1'; - const NFT0 = new BN('10000000000000000000000000'); beforeEach(async function () { this.token = await ERC721VotesMock.new(name, symbol); @@ -89,6 +90,7 @@ contract('ERC721Votes', function (accounts) { this.token.mint(holder, NFT2); this.token.mint(holder, NFT3); this.token.mint(holder, NFT0); + this.token.mint(holder, NFT4); await expectRevert( this.token.mint(holder, lastTokenId), From 7620553bb0d5387e2f342ccaa2e276ab1c9b531f Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 14:53:24 -0400 Subject: [PATCH 045/300] Updating tests setting more NFT voting power to single voter --- test/governance/GovernorWorkflow.behavior.js | 1 - test/governance/extensions/GovernorERC721.test.js | 10 +++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js index 0cc1162790f..402a08358aa 100644 --- a/test/governance/GovernorWorkflow.behavior.js +++ b/test/governance/GovernorWorkflow.behavior.js @@ -71,7 +71,6 @@ function runGovernorWorkflow () { if (tryGet(this.settings, 'voters')) { this.receipts.castVote = []; for (const voter of this.settings.voters) { - console.log('voting',voter); if (!voter.signature) { this.receipts.castVote.push( await getReceiptOrRevert( diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 4c2efb050f8..384299d495b 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -1,4 +1,5 @@ const { expectEvent } = require('@openzeppelin/test-helpers'); +const { BN } = require('bn.js'); const Enums = require('../../helpers/enums'); const { @@ -78,18 +79,21 @@ contract('GovernorERC721Mock', function (accounts) { 'VoteCast', this.settings.voters.find(({ address }) => address === voter), ); - console.log(voter); + if (voter == voter2) { expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('2'); } else { expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); - } + } } await this.mock.proposalVotes(this.id).then(result => { for (const [key, value] of Object.entries(Enums.VoteType)) { expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( - Object.values(this.settings.voters).filter(({ support }) => support === value).length.toString(), + Object.values(this.settings.voters).filter(({ support }) => support === value).reduce( + (acc, {nfts}) => acc.add(new BN(nfts.length)), + new BN('0'), + ), ); } }); From ee1df2c3d033a649202302e9017a4d080e4b1658 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 15:35:32 -0400 Subject: [PATCH 046/300] Adding method to return token voting power --- .../extensions/GovernorVotesERC721.sol | 1 + contracts/mocks/ERC721VotesMock.sol | 5 ++-- contracts/token/ERC721/ERC721.sol | 14 +++------- .../ERC721/extensions/draft-ERC721Votes.sol | 21 ++++++++------- docs/modules/ROOT/pages/governance.adoc | 15 +++++------ openzeppelin-solidity-4.3.2.tgz | Bin 0 -> 199211 bytes test/governance/GovernorWorkflow.behavior.js | 2 +- .../extensions/GovernorERC721.test.js | 24 ++++++++++++++---- 8 files changed, 46 insertions(+), 36 deletions(-) create mode 100644 openzeppelin-solidity-4.3.2.tgz diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 63f62b9775a..29050b1d121 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -14,6 +14,7 @@ import "../../utils/math/Math.sol"; */ abstract contract GovernorVotesERC721 is Governor { ERC721Votes public immutable token; + /** * @dev Need the ERC721Votes address to be initialized */ diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 92184352e51..b47bfd8f24e 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,8 +5,7 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/draft-ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { - - constructor(string memory name, string memory symbol) ERC721Votes(name, symbol) { } + constructor(string memory name, string memory symbol) ERC721Votes(name, symbol) {} function mint(address account, uint256 tokenId) public { _mint(account, tokenId); @@ -20,7 +19,7 @@ contract ERC721VotesMock is ERC721Votes { return block.chainid; } - function _maxSupply() internal pure override returns(uint224){ + function _maxSupply() internal pure override returns (uint224) { return uint224(5); } } diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 2a6cfcd1d02..2dd993e10d5 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -423,23 +423,17 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { address to, uint256 tokenId ) internal virtual {} - - /** + + /** * @dev Hook that is called after any transfer of tokens. This includes * minting and burning. * * Calling conditions: * - * - when `from` and `to` are both non-zero, `tokenId` of ``from``'s tokens - * has been transferred to `to`. - * - when `from` is zero, `tokenId` tokens have been minted for `to`. - * - when `to` is zero, `tokenId` of ``from``'s tokens have been burned. + * - when `from` and `to` are both non-zero. * - `from` and `to` are never both zero. * * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */ - function _afterTokenTransfer( - address from, - address to - ) internal virtual {} + function _afterTokenTransfer(address from, address to) internal virtual {} } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 30d4500e584..42b850d3f7d 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -9,6 +9,7 @@ import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; import "../../../utils/cryptography/draft-EIP712.sol"; + /** * @dev Extension of ERC721 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. @@ -44,9 +45,9 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. * * It's a good idea to use the same `name` that is defined as the ERC721 token name. - */ - constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} - + */ + constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} + /** * @dev Emitted when an account changes their delegate. */ @@ -181,10 +182,10 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _mint(address account, uint256 tokenId) internal virtual override { require(_totalSupply + 1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - + super._mint(account, tokenId); _totalSupply += 1; - + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } @@ -202,10 +203,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { * * Emits a {DelegateVotesChanged} event. */ - function _afterTokenTransfer( - address from, - address to - ) internal virtual override { + function _afterTokenTransfer(address from, address to) internal virtual override { super._afterTokenTransfer(from, to); _moveVotingPower(delegates(from), delegates(to), 1); @@ -286,6 +284,11 @@ abstract contract ERC721Votes is ERC721, EIP712 { return _domainSeparatorV4(); } + /** + * @dev Returns token voting power + */ + function _getVotingPower(uint tokenId) internal virtual returns(uint256) {} + function _add(uint256 a, uint256 b) private pure returns (uint256) { return a + b; } diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 129b7a65580..028070c85a8 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -132,24 +132,23 @@ pragma solidity ^0.8.2; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/draft-ERC721Votes.sol"; -contract MyToken is ERC721, EIP712, ERC721Votes { - constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} +contract MyToken is ERC721Votes { + constructor() ERC721Votes("MyToken", "MTK"){} // The functions below are overrides required by Solidity. function _afterTokenTransfer( address from, - address to, - uint256 tokenId - ) internal override(ERC721, ERC721Votes) { - super._afterTokenTransfer(from, to, tokenId); + address to + ) internal override(ERC721Votes) { + super._afterTokenTransfer(from, to); } - function _mint(address to, uint256 tokenId) internal override(ERC721, ERC721Votes) { + function _mint(address to, uint256 tokenId) internal override(ERC721Votes) { super._mint(to, tokenId); } - function _burn(uint256 tokenId) internal override(ERC721, ERC721Votes) { + function _burn(uint256 tokenId) internal override(ERC721Votes) { super._burn(tokenId); } } diff --git a/openzeppelin-solidity-4.3.2.tgz b/openzeppelin-solidity-4.3.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..927e56027f696044c87e4fb5500a59541497e9c1 GIT binary patch literal 199211 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≪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%0D88Jl0bKxb@g^9a8 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 From 43b69f9c78d37504b5351c47b4fca785069edfcf Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 21:10:40 -0400 Subject: [PATCH 048/300] Update contracts/token/ERC721/extensions/draft-ERC721Votes.sol Updating constructor Co-authored-by: Francisco Giordano --- contracts/token/ERC721/extensions/draft-ERC721Votes.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 42b850d3f7d..09c53e16716 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -46,7 +46,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { * * It's a good idea to use the same `name` that is defined as the ERC721 token name. */ - constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} + constructor(string memory name) EIP712(name, "1") {} /** * @dev Emitted when an account changes their delegate. From 29061447c0e3372d5cd3e8b7ff918f34d918e8e2 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 21:57:59 -0400 Subject: [PATCH 049/300] Including getVotingPower --- contracts/token/ERC721/ERC721.sol | 4 ++-- .../token/ERC721/extensions/draft-ERC721Votes.sol | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 2dd993e10d5..b5b59fcbbfb 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -343,7 +343,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { emit Transfer(from, to, tokenId); - _afterTokenTransfer(from, to); + _afterTokenTransfer(from, to, tokenId); } /** @@ -435,5 +435,5 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */ - function _afterTokenTransfer(address from, address to) internal virtual {} + function _afterTokenTransfer(address from, address to, uint256 tokenId) internal virtual {} } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 09c53e16716..5c2406ee789 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -46,7 +46,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { * * It's a good idea to use the same `name` that is defined as the ERC721 token name. */ - constructor(string memory name) EIP712(name, "1") {} + constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} /** * @dev Emitted when an account changes their delegate. @@ -203,10 +203,10 @@ abstract contract ERC721Votes is ERC721, EIP712 { * * Emits a {DelegateVotesChanged} event. */ - function _afterTokenTransfer(address from, address to) internal virtual override { - super._afterTokenTransfer(from, to); + function _afterTokenTransfer(address from, address to, uint256 tokenId) internal virtual override { + super._afterTokenTransfer(from, to, tokenId); - _moveVotingPower(delegates(from), delegates(to), 1); + _moveVotingPower(delegates(from), delegates(to), _getVotingPower()); } /** @@ -286,8 +286,12 @@ abstract contract ERC721Votes is ERC721, EIP712 { /** * @dev Returns token voting power + * The default token value is 1. To implement a different value + * computation you should override it sending the tokenId. */ - function _getVotingPower(uint tokenId) internal virtual returns(uint256) {} + function _getVotingPower() internal virtual returns(uint256) { + return 1; + } function _add(uint256 a, uint256 b) private pure returns (uint256) { return a + b; From 185c45d46448307e6ceac0cb966ef465617a76c8 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 5 Nov 2021 07:11:59 -0400 Subject: [PATCH 050/300] After prettier run, updating format --- contracts/token/ERC721/ERC721.sol | 6 +++++- .../ERC721/extensions/draft-ERC721Votes.sol | 16 ++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index b5b59fcbbfb..cb75fd9f607 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -435,5 +435,9 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */ - function _afterTokenTransfer(address from, address to, uint256 tokenId) internal virtual {} + function _afterTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual {} } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 5c2406ee789..bbd19cea967 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -203,7 +203,11 @@ abstract contract ERC721Votes is ERC721, EIP712 { * * Emits a {DelegateVotesChanged} event. */ - function _afterTokenTransfer(address from, address to, uint256 tokenId) internal virtual override { + function _afterTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual override { super._afterTokenTransfer(from, to, tokenId); _moveVotingPower(delegates(from), delegates(to), _getVotingPower()); @@ -285,11 +289,11 @@ abstract contract ERC721Votes is ERC721, EIP712 { } /** - * @dev Returns token voting power - * The default token value is 1. To implement a different value - * computation you should override it sending the tokenId. - */ - function _getVotingPower() internal virtual returns(uint256) { + * @dev Returns token voting power + * The default token value is 1. To implement a different value + * computation you should override it sending the tokenId. + */ + function _getVotingPower() internal virtual returns (uint256) { return 1; } From 166edd29b05554008bfd787485b2753488c5d4fa Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 5 Nov 2021 14:38:12 -0400 Subject: [PATCH 051/300] Renaming totalSupply to totalVotingPower for the code to be cleaner --- .../ERC721/extensions/draft-ERC721Votes.sol | 31 ++++++---------- .../ERC721/extensions/ERC721Votes.test.js | 36 +++++++++---------- 2 files changed, 29 insertions(+), 38 deletions(-) diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index bbd19cea967..f25b6e2271f 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -32,14 +32,14 @@ abstract contract ERC721Votes is ERC721, EIP712 { uint32 fromBlock; uint224 votes; } - uint256 _totalSupply; + uint256 _totalVotingPower; bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); mapping(address => address) private _delegates; mapping(address => Counters.Counter) private _nonces; mapping(address => Checkpoint[]) private _checkpoints; - Checkpoint[] private _totalSupplyCheckpoints; + Checkpoint[] private _totalVotingPowerCheckpoints; /** * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. @@ -100,16 +100,16 @@ abstract contract ERC721Votes is ERC721, EIP712 { } /** - * @dev Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances. + * @dev Retrieve the `totalVotingPower` at the end of `blockNumber`. Note, this value is the sum of all balances. * It is but NOT the sum of all the delegated votes! * * Requirements: * * - `blockNumber` must have been already mined */ - function getPastTotalSupply(uint256 blockNumber) public view returns (uint256) { + function getPastVotingPower(uint256 blockNumber) public view returns (uint256) { require(blockNumber < block.number, "ERC721Votes: block not yet mined"); - return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber); + return _checkpointsLookup(_totalVotingPowerCheckpoints, blockNumber); } /** @@ -181,12 +181,12 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been increased. */ function _mint(address account, uint256 tokenId) internal virtual override { - require(_totalSupply + 1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + require(_totalVotingPower + 1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); super._mint(account, tokenId); - _totalSupply += 1; + _totalVotingPower += 1; - _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); + _writeCheckpoint(_totalVotingPowerCheckpoints, _add, 1); } /** @@ -194,8 +194,8 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); - _totalSupply -= 1; - _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); + _totalVotingPower -= 1; + _writeCheckpoint(_totalVotingPowerCheckpoints, _subtract, 1); } /** @@ -210,7 +210,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { ) internal virtual override { super._afterTokenTransfer(from, to, tokenId); - _moveVotingPower(delegates(from), delegates(to), _getVotingPower()); + _moveVotingPower(delegates(from), delegates(to), 1); } /** @@ -288,15 +288,6 @@ abstract contract ERC721Votes is ERC721, EIP712 { return _domainSeparatorV4(); } - /** - * @dev Returns token voting power - * The default token value is 1. To implement a different value - * computation you should override it sending the tokenId. - */ - function _getVotingPower() internal virtual returns (uint256) { - return 1; - } - function _add(uint256 a, uint256 b) private pure returns (uint256) { return a + b; } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 196af93ec83..81397dda8aa 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -489,20 +489,20 @@ contract('ERC721Votes', function (accounts) { }); }); - describe('getPastTotalSupply', function () { + describe('getPastVotingPower', 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), + this.token.getPastVotingPower(5e10), 'ERC721Votes: block not yet mined', ); }); it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(0)).to.be.bignumber.equal('0'); }); it('returns the latest block if >= last checkpoint block', async function () { @@ -511,8 +511,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('returns zero if < first checkpoint block', async function () { @@ -521,8 +521,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { @@ -542,17 +542,17 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); }); }); From b0908c11b74119d958c91071eb618b533a03101e Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sat, 30 Oct 2021 20:49:29 -0400 Subject: [PATCH 052/300] Initial contracts creation --- .../extensions/GovernorVotesERC721.sol | 28 ++ .../token/ERC721/extensions/ERC721Votes.sol | 258 ++++++++++++++++++ .../ERC721/extensions/draft-ERC721Permit.sol | 87 ++++++ .../ERC721/extensions/draft-IERC721Permit.sol | 60 ++++ 4 files changed, 433 insertions(+) create mode 100644 contracts/governance/extensions/GovernorVotesERC721.sol create mode 100644 contracts/token/ERC721/extensions/ERC721Votes.sol create mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol create mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol new file mode 100644 index 00000000000..9a2a4ac0ee8 --- /dev/null +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC72GovernorVotesERC721.sol) + +pragma solidity ^0.8.0; + +import "../Governor.sol"; +import "../../token/ERC721/extensions/ERC721Votes.sol"; +import "../../utils/math/Math.sol"; + +/** + * @dev Extension of {Governor} for voting weight extraction from an {ERC721Votes} token. + * + * _Available since v4.3._ + */ +abstract contract ERC72GovernorVotesERC721 is Governor { + ERC721Votes public immutable token; + + constructor(ERC721Votes tokenAddress) { + token = tokenAddress; + } + + /** + * Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). + */ + function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { + return token.getPastVotes(account, blockNumber); + } +} diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol new file mode 100644 index 00000000000..20da35f32a6 --- /dev/null +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/ERC721Votes.sol) + +pragma solidity ^0.8.0; + +import "./draft-ERC721Permit.sol"; +import "../../../utils/math/Math.sol"; +import "../../../utils/math/SafeCast.sol"; +import "../../../utils/cryptography/ECDSA.sol"; + +/** + * @dev Extension of ERC721 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 {ERC721VotesComp} 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 {getVotes} and {getPastVotes}. + * + * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it + * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. + * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this + * will significantly increase the base gas cost of transfers. + * + * _Available since v4.2._ + */ +abstract contract ERC721Votes is ERC721Permit { + struct Checkpoint { + uint32 fromBlock; + uint224 votes; + } + + bytes32 private constant _DELEGATION_TYPEHASH = + keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + + mapping(address => address) private _delegates; + mapping(address => Checkpoint[]) private _checkpoints; + Checkpoint[] private _totalSupplyCheckpoints; + + /** + * @dev Emitted when an account changes their delegate. + */ + event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); + + /** + * @dev Emitted when a token transfer or delegate change results in changes to an account's voting power. + */ + event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); + + /** + * @dev Get the `pos`-th checkpoint for `account`. + */ + function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoint memory) { + return _checkpoints[account][pos]; + } + + /** + * @dev Get number of checkpoints for `account`. + */ + function numCheckpoints(address account) public view virtual returns (uint32) { + return SafeCast.toUint32(_checkpoints[account].length); + } + + /** + * @dev Get the address `account` is currently delegating to. + */ + function delegates(address account) public view virtual returns (address) { + return _delegates[account]; + } + + /** + * @dev Gets the current votes balance for `account` + */ + function getVotes(address account) public view returns (uint256) { + uint256 pos = _checkpoints[account].length; + return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes; + } + + /** + * @dev Retrieve the number of votes for `account` at the end of `blockNumber`. + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) { + require(blockNumber < block.number, "ERC721Votes: block not yet mined"); + return _checkpointsLookup(_checkpoints[account], blockNumber); + } + + /** + * @dev Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances. + * It is but NOT the sum of all the delegated votes! + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastTotalSupply(uint256 blockNumber) public view returns (uint256) { + require(blockNumber < block.number, "ERC721Votes: block not yet mined"); + return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber); + } + + /** + * @dev Lookup a value in a list of (sorted) checkpoints. + */ + function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) private view returns (uint256) { + // We run a binary search to look for the earliest checkpoint taken after `blockNumber`. + // + // During the loop, the index of the wanted checkpoint remains in the range [low-1, high). + // With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant. + // - If the middle checkpoint is after `blockNumber`, we look in [low, mid) + // - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high) + // Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not + // out of bounds (in which case we're looking too far in the past and the result is 0). + // Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is + // past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out + // the same. + uint256 high = ckpts.length; + uint256 low = 0; + while (low < high) { + uint256 mid = Math.average(low, high); + if (ckpts[mid].fromBlock > blockNumber) { + high = mid; + } else { + low = mid + 1; + } + } + + return high == 0 ? 0 : ckpts[high - 1].votes; + } + + /** + * @dev Delegate votes from the sender to `delegatee`. + */ + function delegate(address delegatee) public virtual { + _delegate(_msgSender(), delegatee); + } + + /** + * @dev Delegates votes from signer to `delegatee` + */ + function delegateBySig( + address delegatee, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + require(block.timestamp <= expiry, "ERC721Votes: signature expired"); + address signer = ECDSA.recover( + _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), + v, + r, + s + ); + require(nonce == _useNonce(signer), "ERC721Votes: invalid nonce"); + _delegate(signer, delegatee); + } + + /** + * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). + */ + function _maxSupply() internal view virtual returns (uint224) { + return type(uint224).max; + } + + /** + * @dev Snapshots the totalSupply after it has been increased. + */ + function _mint(address account, uint256 amount) internal virtual override { + super._mint(account, amount);//TODO: update for NFT + require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + + _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); + } + + /** + * @dev Snapshots the totalSupply after it has been decreased. + */ + function _burn(uint256 tokenId) internal virtual override { + super._burn(tokenId); + + _writeCheckpoint(_totalSupplyCheckpoints, _subtract, tokenId); + } + + /** + * @dev Move voting power when tokens are transferred. + * + * Emits a {DelegateVotesChanged} event. + */ + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual { + _moveVotingPower(delegates(from), delegates(to), amount);//TODO: Update to be NFT logic + } + + /** + * @dev Change delegation for `delegator` to `delegatee`. + * + * Emits events {DelegateChanged} and {DelegateVotesChanged}. + */ + function _delegate(address delegator, address delegatee) internal virtual { + address currentDelegate = delegates(delegator); + uint256 delegatorBalance = balanceOf(delegator); + _delegates[delegator] = delegatee; + + emit DelegateChanged(delegator, currentDelegate, delegatee); + + _moveVotingPower(currentDelegate, delegatee, delegatorBalance); + } + + function _moveVotingPower( + address src, + address dst, + uint256 amount + ) private { + if (src != dst && amount > 0) { + if (src != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[src], _subtract, amount); + emit DelegateVotesChanged(src, oldWeight, newWeight); + } + + if (dst != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[dst], _add, amount); + emit DelegateVotesChanged(dst, oldWeight, newWeight); + } + } + } + + function _writeCheckpoint(//TODO: update for NFT + Checkpoint[] storage ckpts, + function(uint256, uint256) view returns (uint256) op, + uint256 delta + ) private returns (uint256 oldWeight, uint256 newWeight) { + uint256 pos = ckpts.length; + oldWeight = pos == 0 ? 0 : ckpts[pos - 1].votes; + newWeight = op(oldWeight, delta); + + if (pos > 0 && ckpts[pos - 1].fromBlock == block.number) { + ckpts[pos - 1].votes = SafeCast.toUint224(newWeight); + } else { + ckpts.push(Checkpoint({fromBlock: SafeCast.toUint32(block.number), votes: SafeCast.toUint224(newWeight)})); + } + } + + function _add(uint256 a, uint256 b) private pure returns (uint256) { + return a + b; + } + + function _subtract(uint256 a, uint256 b) private pure returns (uint256) { + return a - b; + } +} diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol new file mode 100644 index 00000000000..c00d15367ab --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) + +pragma solidity ^0.8.0; + +import "./draft-IERC721Permit.sol"; +import "./ERC721Enumerable.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; +import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/Counters.sol"; + +/** + * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * _Available since v3.4._ + */ +abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { + using Counters for Counters.Counter; + + mapping(address => Counters.Counter) private _nonces; + + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC721 token name. + */ + constructor(string memory name) EIP712(name, "1") {} + + /** + * @dev See {IERC721Permit-permit}. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); + + bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + + bytes32 hash = _hashTypedDataV4(structHash); + + address signer = ECDSA.recover(hash, v, r, s); + require(signer == owner, "ERC721Permit: invalid signature"); + + _approve(spender, value); + } + + /** + * @dev See {IERC721Permit-nonces}. + */ + function nonces(address owner) public view virtual override returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } +} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol new file mode 100644 index 00000000000..61882f2de0d --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + */ +interface IERC721Permit { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC721-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} From 98060a37b4c1a49f67f215e5b96a6ee841090c7b Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 09:28:35 -0400 Subject: [PATCH 053/300] creating permit tests --- contracts/mocks/ERC721PermitMock.sol | 20 + contracts/mocks/ERC721VotesMock.sol | 21 + .../ERC721/extensions/ERC721Votes.test.js | 538 ++++++++++++++++++ .../extensions/draft-ERC721Permit.test.js | 117 ++++ 4 files changed, 696 insertions(+) create mode 100644 contracts/mocks/ERC721PermitMock.sol create mode 100644 contracts/mocks/ERC721VotesMock.sol create mode 100644 test/token/ERC721/extensions/ERC721Votes.test.js create mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol new file mode 100644 index 00000000000..37a860ef4dc --- /dev/null +++ b/contracts/mocks/ERC721PermitMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC721/extensions/draft-ERC721Permit.sol"; + +contract ERC721PermitMock is ERC721Permit { + constructor( + string memory name, + string memory symbol, + address initialAccount, + uint256 tokenId + ) payable ERC721(name, symbol) ERC721Permit(name) { + _mint(initialAccount, tokenId); + } + + function getChainId() external view returns (uint256) { + return block.chainid; + } +} diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol new file mode 100644 index 00000000000..457d05eedb9 --- /dev/null +++ b/contracts/mocks/ERC721VotesMock.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC721/extensions/ERC721Votes.sol"; + +contract ERC721VotesMock is ERC721Votes { + constructor(string memory name, string memory symbol) ERC721(name, symbol) ERC721Permit(name) {} + + function mint(address account, uint256 tokenId) public { + _mint(account, tokenId); + } + + function burn(uint256 tokenId) public { + _burn(tokenId); + } + + function getChainId() external view returns (uint256) { + return block.chainid; + } +} diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js new file mode 100644 index 00000000000..7078828039d --- /dev/null +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -0,0 +1,538 @@ +/* eslint-disable */ + +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const { promisify } = require('util'); +const queue = promisify(setImmediate); + +const ERC721VotesMock = artifacts.require('ERC721VotesMock'); + +const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); + +const Delegation = [ + { name: 'delegatee', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'expiry', type: 'uint256' }, +]; + +async function countPendingTransactions() { + return parseInt( + await network.provider.send('eth_getBlockTransactionCountByNumber', ['pending']) + ); +} + +async function batchInBlock (txs) { + try { + // disable auto-mining + await network.provider.send('evm_setAutomine', [false]); + // send all transactions + const promises = txs.map(fn => fn()); + // wait for node to have all pending transactions + while (txs.length > await countPendingTransactions()) { + await queue(); + } + // mine one block + await network.provider.send('evm_mine'); + // fetch receipts + const receipts = await Promise.all(promises); + // Sanity check, all tx should be in the same block + const minedBlocks = new Set(receipts.map(({ receipt }) => receipt.blockNumber)); + expect(minedBlocks.size).to.equal(1); + + return receipts; + } finally { + // enable auto-mining + await network.provider.send('evm_setAutomine', [true]); + } +} + +contract('ERC721Votes', function (accounts) { + const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; + const supply = new BN('10000000000000000000000000'); + + beforeEach(async function () { + this.token = await ERC721VotesMock.new(name, symbol); + + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = await this.token.getChainId(); + }); + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect( + await this.token.DOMAIN_SEPARATOR(), + ).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address), + ); + }); + + it('minting restriction', async function () { + const amount = new BN('2').pow(new BN('224')); + await expectRevert( + this.token.mint(holder, amount), + 'ERC721Votes: total supply risks overflowing votes', + ); + }); + + describe('set delegation', function () { + describe('call', function () { + it('delegation with balance', async function () { + await this.token.mint(holder, supply); + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + + expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); + }); + + it('delegation without balance', async function () { + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + }); + }); + + describe('with signature', function () { + const delegator = Wallet.generate(); + const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); + const nonce = 0; + + const buildData = (chainId, verifyingContract, message) => ({ data: { + primaryType: 'Delegation', + types: { EIP712Domain, Delegation }, + domain: { name, version, chainId, verifyingContract }, + message, + }}); + + beforeEach(async function () { + await this.token.mint(delegatorAddress, supply); + }); + + it('accept signed delegation', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + expectEvent(receipt, 'DelegateChanged', { + delegator: delegatorAddress, + fromDelegate: ZERO_ADDRESS, + toDelegate: delegatorAddress, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: delegatorAddress, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); + + expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); + }); + + it('rejects reused signature', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), + 'ERC721Votes: invalid nonce', + ); + }); + + it('rejects bad delegatee', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + const { logs } = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); + const { args } = logs.find(({ event }) => event == 'DelegateChanged'); + expect(args.delegator).to.not.be.equal(delegatorAddress); + expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); + expect(args.toDelegate).to.be.equal(holderDelegatee); + }); + + it('rejects bad nonce', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), + 'ERC721Votes: invalid nonce', + ); + }); + + it('rejects expired permit', async function () { + const expiry = (await time.latest()) - time.duration.weeks(1); + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry, + }), + )); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), + 'ERC721Votes: signature expired', + ); + }); + }); + }); + + describe('change delegation', function () { + beforeEach(async function () { + await this.token.mint(holder, supply); + await this.token.delegate(holder, { from: holder }); + }); + + it('call', async function () { + expect(await this.token.delegates(holder)).to.be.equal(holder); + + const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: holder, + toDelegate: holderDelegatee, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: supply, + newBalance: '0', + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holderDelegatee, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); + + expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); + expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); + }); + }); + + 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.getVotes(holder)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); + + // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" + const blockNumber = await time.latestBlock(); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getPastVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes); + }); + }); + + // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js. + describe('Compound test suite', function () { + beforeEach(async function () { + await this.token.mint(holder, supply); + }); + + describe('balanceOf', function () { + it('grants to initial account', async function () { + expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); + }); + }); + + describe('numCheckpoints', function () { + it('returns the number of checkpoints for a delegate', async function () { + await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const t1 = await this.token.delegate(other1, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + + const t2 = await this.token.transfer(other2, 10, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + + const t3 = await this.token.transfer(other2, 10, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); + + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '100' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '90' ]); + expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '80' ]); + expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + + await time.advanceBlock(); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); + }); + + it('does not add more than one checkpoint in a block', async function () { + await this.token.transfer(recipient, '100', { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const [ t1, t2, t3 ] = await batchInBlock([ + () => this.token.delegate(other1, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + ]); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); + // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check + // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + }); + }); + + describe('getPastVotes', function () { + it('reverts if block number >= current block', async function () { + await expectRevert( + this.token.getPastVotes(other1, 5e10), + 'ERC721Votes: block not yet mined', + ); + }); + + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.transfer(holder, 20, { from: other2 }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + }); + }); + + describe('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), + 'ERC721Votes: block not yet mined', + ); + }); + + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + t1 = await this.token.mint(holder, supply); + + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const t1 = await this.token.mint(holder, supply); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.mint(holder, supply); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.burn(10); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.burn(10); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.mint(holder, 20); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + }); +}); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js new file mode 100644 index 00000000000..7a7b8cf5d83 --- /dev/null +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -0,0 +1,117 @@ +/* eslint-disable */ + +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const ERC721PermitMock = artifacts.require('ERC721PermitMock'); + +const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); + +const Permit = [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, +]; + +contract('ERC721Permit', function (accounts) { + const [ initialHolder, spender, recipient, other ] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; + + const initialTokenId = new BN('100'); + + beforeEach(async function () { + this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); + + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = await this.token.getChainId(); + }); + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect( + await this.token.DOMAIN_SEPARATOR(), + ).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address), + ); + }); + + describe.only('permit', function () { + const wallet = Wallet.generate(); + + const owner = wallet.getAddressString(); + const value = initialTokenId; + const nonce = 0; + const maxDeadline = MAX_UINT256; + + const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ + primaryType: 'Permit', + types: { EIP712Domain, Permit }, + domain: { name, version, chainId, verifyingContract }, + message: { owner, spender, value, nonce, deadline }, + }); + + it('accepts owner signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); + expect(await this.token.getApproved(value)).to.be.equal(spender); + }); + + it('rejects reused signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects other signature', async function () { + const otherWallet = Wallet.generate(); + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects expired permit', async function () { + const deadline = (await time.latest()) - time.duration.weeks(1); + + const data = buildData(this.chainId, this.token.address, deadline); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, deadline, v, r, s), + 'ERC721Permit: expired deadline', + ); + }); + }); +}); From c26bf6e67a2a75dfaa360cd86db90e6b38d4a658 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 10:26:22 -0400 Subject: [PATCH 054/300] Fixing checkpoints count --- .../token/ERC721/extensions/ERC721Votes.sol | 11 ++--- .../ERC721/extensions/ERC721Votes.test.js | 47 ++++++++++++------- .../extensions/draft-ERC721Permit.test.js | 2 +- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 20da35f32a6..b184c9c83fa 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -7,7 +7,6 @@ import "./draft-ERC721Permit.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; - /** * @dev Extension of ERC721 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. @@ -170,11 +169,11 @@ abstract contract ERC721Votes is ERC721Permit { /** * @dev Snapshots the totalSupply after it has been increased. */ - function _mint(address account, uint256 amount) internal virtual override { - super._mint(account, amount);//TODO: update for NFT + function _mint(address account, uint256 tokenId) internal virtual override { + super._mint(account, tokenId); require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } /** @@ -183,7 +182,7 @@ abstract contract ERC721Votes is ERC721Permit { function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); - _writeCheckpoint(_totalSupplyCheckpoints, _subtract, tokenId); + _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); } /** @@ -232,7 +231,7 @@ abstract contract ERC721Votes is ERC721Permit { } } - function _writeCheckpoint(//TODO: update for NFT + function _writeCheckpoint( Checkpoint[] storage ckpts, function(uint256, uint256) view returns (uint256) op, uint256 delta diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 7078828039d..d2981dd5f9d 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -54,7 +54,9 @@ async function batchInBlock (txs) { contract('ERC721Votes', function (accounts) { const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; - + const NFT1 = new BN('10'); + const NFT2 = new BN('20'); + const NFT3 = new BN('30'); const name = 'My Token'; const symbol = 'MTKN'; const version = '1'; @@ -91,7 +93,7 @@ contract('ERC721Votes', function (accounts) { describe('set delegation', function () { describe('call', function () { - it('delegation with balance', async function () { + it('delegation with balance', async function () {//TODO: Make it NFT like await this.token.mint(holder, supply); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); @@ -249,7 +251,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, supply);//TODO: Avoid tokenId duplicate await this.token.delegate(holder, { from: holder }); }); @@ -359,6 +361,9 @@ contract('ERC721Votes', function (accounts) { describe('Compound test suite', function () { beforeEach(async function () { await this.token.mint(holder, supply); + await this.token.mint(holder, NFT1); + await this.token.mint(holder, NFT2); + await this.token.mint(holder, NFT3); }); describe('balanceOf', function () { @@ -448,16 +453,16 @@ contract('ERC721Votes', function (accounts) { }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); + const t1 = await this.token.delegate(other1, { from: holder });//TODO: Make it NFT like 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 }); + const t3 = await this.token.transfer(other2, 20, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 20, { from: other2 }); + const t4 = await this.token.transfer(holder, 30, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); @@ -511,28 +516,34 @@ contract('ERC721Votes', function (accounts) { }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.mint(holder, supply); + const t1 = await this.token.mint(holder, NFT1); await time.advanceBlock(); await time.advanceBlock(); - const t2 = await this.token.burn(10); + const t2 = await this.token.burn(NFT1); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.burn(10); + const t3 = await this.token.mint(holder, NFT2); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.mint(holder, 20); + const t4 = await this.token.burn(NFT2); + await time.advanceBlock(); + await time.advanceBlock(); + const t5 = await this.token.mint(holder, NFT3); await time.advanceBlock(); await time.advanceBlock(); + console.log(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); }); }); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js index 7a7b8cf5d83..f22c904c724 100644 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -50,7 +50,7 @@ contract('ERC721Permit', function (accounts) { ); }); - describe.only('permit', function () { + describe('permit', function () { const wallet = Wallet.generate(); const owner = wallet.getAddressString(); From 26ed1fed1ae4161406ae302ac14655bbef3b5a3f Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:20:14 -0400 Subject: [PATCH 055/300] Updating ERC721Vote tests --- contracts/mocks/ERC721VotesMock.sol | 4 + .../token/ERC721/extensions/ERC721Votes.sol | 18 ++- .../ERC721/extensions/ERC721Votes.test.js | 146 +++++++++--------- 3 files changed, 96 insertions(+), 72 deletions(-) diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 457d05eedb9..09e40a4ea85 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -18,4 +18,8 @@ contract ERC721VotesMock is ERC721Votes { function getChainId() external view returns (uint256) { return block.chainid; } + + function _maxSupply() internal pure override returns(uint224){ + return uint224(4); + } } diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index b184c9c83fa..146ff1dd213 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -192,10 +192,9 @@ abstract contract ERC721Votes is ERC721Permit { */ function _afterTokenTransfer( address from, - address to, - uint256 amount + address to ) internal virtual { - _moveVotingPower(delegates(from), delegates(to), amount);//TODO: Update to be NFT logic + _moveVotingPower(delegates(from), delegates(to), 1); } /** @@ -254,4 +253,17 @@ abstract contract ERC721Votes is ERC721Permit { function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } + + /** + * @dev Moves token from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 tokenId) external returns (bool){ + _transfer(_msgSender(), recipient, tokenId); + _afterTokenTransfer(_msgSender(), recipient); + return true; + } } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index d2981dd5f9d..b2d56ea7ab0 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -14,6 +14,7 @@ const queue = promisify(setImmediate); const ERC721VotesMock = artifacts.require('ERC721VotesMock'); const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); +const { Console } = require('console'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -84,9 +85,14 @@ contract('ERC721Votes', function (accounts) { }); it('minting restriction', async function () { - const amount = new BN('2').pow(new BN('224')); + const lastTokenId = new BN('2').pow(new BN('224')); + this.token.mint(holder, NFT1); + this.token.mint(holder, NFT2); + this.token.mint(holder, NFT3); + this.token.mint(holder, supply); + await expectRevert( - this.token.mint(holder, amount), + this.token.mint(holder, lastTokenId), 'ERC721Votes: total supply risks overflowing votes', ); }); @@ -106,15 +112,15 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '0', - newBalance: supply, + newBalance: '1', }); expect(await this.token.delegates(holder)).to.be.equal(holder); - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); + expect(await this.token.getVotes(holder)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); }); it('delegation without balance', async function () { @@ -169,15 +175,15 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: delegatorAddress, previousBalance: '0', - newBalance: supply, + newBalance: '1', }); expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); - expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); + expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1'); }); it('rejects reused signature', async function () { @@ -251,7 +257,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { - await this.token.mint(holder, supply);//TODO: Avoid tokenId duplicate + await this.token.mint(holder, supply); await this.token.delegate(holder, { from: holder }); }); @@ -266,24 +272,24 @@ contract('ERC721Votes', function (accounts) { }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, - previousBalance: supply, + previousBalance: '1', newBalance: '0', }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holderDelegatee, previousBalance: '0', - newBalance: supply, + newBalance: '1', }); expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); - expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); + expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); }); }); @@ -293,8 +299,8 @@ contract('ERC721Votes', function (accounts) { }); it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -304,22 +310,22 @@ contract('ERC721Votes', function (accounts) { 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 { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); 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.holderVotes = '0'; 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' }); + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -333,15 +339,15 @@ contract('ERC721Votes', function (accounts) { 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) }); + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); 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.holderVotes = '0'; this.recipientVotes = '1'; }); @@ -368,54 +374,55 @@ contract('ERC721Votes', function (accounts) { describe('balanceOf', function () { it('grants to initial account', async function () { - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('4'); }); }); 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 + + await this.token.transfer(recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transfer(recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - - const t2 = await this.token.transfer(other2, 10, { from: recipient }); + + const t2 = await this.token.transfer(other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - - const t3 = await this.token.transfer(other2, 10, { from: recipient }); + + const t3 = await this.token.transfer(other2, NFT2, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - const t4 = await this.token.transfer(recipient, 20, { from: holder }); + const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '100' ]); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '90' ]); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '80' ]); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); + expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); + expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('1'); }); it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, '100', { from: holder }); + await this.token.transfer(recipient, NFT1, { from: holder }); + await this.token.transfer(recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const [ t1, t2, t3 ] = await batchInBlock([ () => this.token.delegate(other1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, NFT1, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, NFT2, { from: recipient, gas: 100000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); - // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - const t4 = await this.token.transfer(recipient, 20, { from: holder }); + const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); }); @@ -438,8 +445,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('4'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); }); it('returns zero if < first checkpoint block', async function () { @@ -449,39 +456,41 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder });//TODO: Make it NFT like - await time.advanceBlock(); + const total = await this.token.balanceOf(holder); + + const t1 = await this.token.delegate(other1, { from: holder }); await time.advanceBlock(); - const t2 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + const t2 = await this.token.transfer(other2, NFT1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.transfer(other2, 20, { from: holder }); + const t3 = await this.token.transfer(other2, NFT2, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 30, { from: other2 }); + const t4 = await this.token.transfer(holder, NFT2, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); - + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal(total); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(total); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); }); }); }); describe('getPastTotalSupply', function () { beforeEach(async function () { - await this.token.delegate(holder, { from: holder }); + // await this.token.delegate(holder, { from: holder }); }); it('reverts if block number >= current block', async function () { @@ -501,8 +510,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('returns zero if < first checkpoint block', async function () { @@ -512,7 +521,7 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { @@ -532,7 +541,6 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - console.log(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); From c4db7f3763c22eaf34b24245ba003a8dff8593bd Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:23:31 -0400 Subject: [PATCH 056/300] Updating ERC721Vote tests descriptions --- test/token/ERC721/extensions/ERC721Votes.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index b2d56ea7ab0..49592f36fc8 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -99,7 +99,7 @@ contract('ERC721Votes', function (accounts) { describe('set delegation', function () { describe('call', function () { - it('delegation with balance', async function () {//TODO: Make it NFT like + it('delegation with tokenId', async function () { await this.token.mint(holder, supply); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); @@ -123,7 +123,7 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); }); - it('delegation without balance', async function () { + it('delegation without tokenId', async function () { expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); From 0e65ad2c8604d3edc78c385a0a3abcefac88d44f Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:55:45 -0400 Subject: [PATCH 057/300] Updating ERC721Vote contract and tests --- contracts/token/ERC721/extensions/ERC721Votes.sol | 2 -- test/token/ERC721/extensions/ERC721Votes.test.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 146ff1dd213..978bcd0787e 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -11,8 +11,6 @@ import "../../../utils/cryptography/ECDSA.sol"; * @dev Extension of ERC721 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 {ERC721VotesComp} 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 {getVotes} and {getPastVotes}. diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 49592f36fc8..9712e69d21e 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -490,7 +490,7 @@ contract('ERC721Votes', function (accounts) { describe('getPastTotalSupply', function () { beforeEach(async function () { - // await this.token.delegate(holder, { from: holder }); + await this.token.delegate(holder, { from: holder }); }); it('reverts if block number >= current block', async function () { From f0bc5225fd8b4b19b612665409698c1f92c3b3e8 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 14:54:43 -0400 Subject: [PATCH 058/300] Finished tests --- test/token/ERC721/extensions/ERC721Votes.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 9712e69d21e..6d705c7c80f 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -415,16 +415,16 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const [ t1, t2, t3 ] = await batchInBlock([ - () => this.token.delegate(other1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, NFT1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, NFT2, { from: recipient, gas: 100000 }), + () => this.token.delegate(other1, { from: recipient, gas: 200000 }), + () => this.token.transfer(other2, NFT1, { from: recipient, gas: 200000 }), + () => this.token.transfer(other2, NFT2, { from: recipient, gas: 200000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); }); From 71405a59158833a382ee239dea0faed4c5b92238 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 16:24:20 -0400 Subject: [PATCH 059/300] Adding _afterTokenTransfer to base ERC271 contract --- .../extensions/GovernorVotesERC721.sol | 4 +- contracts/token/ERC721/ERC721.sol | 22 ++++ .../token/ERC721/extensions/ERC721Votes.sol | 19 +-- .../ERC721/extensions/ERC721Votes.test.js | 34 ++--- .../extensions/draft-ERC721Permit.test.js | 117 ------------------ 5 files changed, 44 insertions(+), 152 deletions(-) delete mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 9a2a4ac0ee8..383960d9ef1 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC72GovernorVotesERC721.sol) +// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC721GovernorVotesERC721.sol) pragma solidity ^0.8.0; @@ -12,7 +12,7 @@ import "../../utils/math/Math.sol"; * * _Available since v4.3._ */ -abstract contract ERC72GovernorVotesERC721 is Governor { +abstract contract ERC721GovernorVotesERC721 is Governor { ERC721Votes public immutable token; constructor(ERC721Votes tokenAddress) { diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index dbd91bcbcb8..1e9a88a55db 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -342,6 +342,8 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { _owners[tokenId] = to; emit Transfer(from, to, tokenId); + + _afterTokenTransfer(from, to, tokenId); } /** @@ -421,4 +423,24 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { address to, uint256 tokenId ) internal virtual {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `tokenId` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `tokenId` tokens have been minted for `to`. + * - when `to` is zero, `tokenId` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual {} } diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 978bcd0787e..18164dc6721 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; -import "./draft-ERC721Permit.sol"; +import "../ERC721.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; @@ -22,7 +22,7 @@ import "../../../utils/cryptography/ECDSA.sol"; * * _Available since v4.2._ */ -abstract contract ERC721Votes is ERC721Permit { +abstract contract ERC721Votes is ERC721 { struct Checkpoint { uint32 fromBlock; uint224 votes; @@ -191,7 +191,7 @@ abstract contract ERC721Votes is ERC721Permit { function _afterTokenTransfer( address from, address to - ) internal virtual { + ) internal virtual override{ _moveVotingPower(delegates(from), delegates(to), 1); } @@ -251,17 +251,4 @@ abstract contract ERC721Votes is ERC721Permit { function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } - - /** - * @dev Moves token from the caller's account to `recipient`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 tokenId) external returns (bool){ - _transfer(_msgSender(), recipient, tokenId); - _afterTokenTransfer(_msgSender(), recipient); - return true; - } } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 6d705c7c80f..1a114c1c6b2 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -299,7 +299,7 @@ contract('ERC721Votes', function (accounts) { }); it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); @@ -310,7 +310,7 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); @@ -324,7 +324,7 @@ contract('ERC721Votes', function (accounts) { it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -339,7 +339,7 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -381,20 +381,20 @@ contract('ERC721Votes', function (accounts) { describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { - await this.token.transfer(recipient, NFT1, { from: holder }); //give an account two tokens for readability - await this.token.transfer(recipient, NFT2, { from: holder }); + await this.token.transferFrom(recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transferFrom(recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - const t2 = await this.token.transfer(other2, NFT1, { from: recipient }); + const t2 = await this.token.transferFrom(other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - const t3 = await this.token.transfer(other2, NFT2, { from: recipient }); + const t3 = await this.token.transferFrom(other2, NFT2, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); @@ -410,19 +410,19 @@ contract('ERC721Votes', function (accounts) { }); it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, NFT1, { from: holder }); - await this.token.transfer(recipient, NFT2, { from: holder }); + await this.token.transferFrom(recipient, NFT1, { from: holder }); + await this.token.transferFrom(recipient, NFT2, { 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, NFT1, { from: recipient, gas: 200000 }), - () => this.token.transfer(other2, NFT2, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(other2, NFT1, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(other2, NFT2, { from: recipient, gas: 200000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); - const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); @@ -465,13 +465,13 @@ contract('ERC721Votes', function (accounts) { const t1 = await this.token.delegate(other1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t2 = await this.token.transfer(other2, NFT1, { from: holder }); + const t2 = await this.token.transferFrom(other2, NFT1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.transfer(other2, NFT2, { from: holder }); + const t3 = await this.token.transferFrom(other2, NFT2, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transfer(holder, NFT2, { from: other2 }); + const t4 = await this.token.transferFrom(holder, NFT2, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js deleted file mode 100644 index f22c904c724..00000000000 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable */ - -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); -const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; - -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; - -const ERC721PermitMock = artifacts.require('ERC721PermitMock'); - -const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); - -const Permit = [ - { name: 'owner', type: 'address' }, - { name: 'spender', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint256' }, -]; - -contract('ERC721Permit', function (accounts) { - const [ initialHolder, spender, recipient, other ] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - - const initialTokenId = new BN('100'); - - beforeEach(async function () { - this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); - - // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id - // from within the EVM as from the JSON RPC interface. - // See https://github.com/trufflesuite/ganache-core/issues/515 - this.chainId = await this.token.getChainId(); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect( - await this.token.DOMAIN_SEPARATOR(), - ).to.equal( - await domainSeparator(name, version, this.chainId, this.token.address), - ); - }); - - describe('permit', function () { - const wallet = Wallet.generate(); - - const owner = wallet.getAddressString(); - const value = initialTokenId; - const nonce = 0; - const maxDeadline = MAX_UINT256; - - const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ - primaryType: 'Permit', - types: { EIP712Domain, Permit }, - domain: { name, version, chainId, verifyingContract }, - message: { owner, spender, value, nonce, deadline }, - }); - - it('accepts owner signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); - expect(await this.token.getApproved(value)).to.be.equal(spender); - }); - - it('rejects reused signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects other signature', async function () { - const otherWallet = Wallet.generate(); - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects expired permit', async function () { - const deadline = (await time.latest()) - time.duration.weeks(1); - - const data = buildData(this.chainId, this.token.address, deadline); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, deadline, v, r, s), - 'ERC721Permit: expired deadline', - ); - }); - }); -}); From ad3c2f8f3353450360a680ed4b04232762c3b476 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 16:30:31 -0400 Subject: [PATCH 060/300] Renaming supplied tokenId on tests --- .../ERC721/extensions/draft-ERC721Permit.sol | 87 ------------------- .../ERC721/extensions/draft-IERC721Permit.sol | 60 ------------- .../ERC721/extensions/ERC721Votes.test.js | 34 ++++---- 3 files changed, 17 insertions(+), 164 deletions(-) delete mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol delete mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol deleted file mode 100644 index c00d15367ab..00000000000 --- a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) - -pragma solidity ^0.8.0; - -import "./draft-IERC721Permit.sol"; -import "./ERC721Enumerable.sol"; -import "../../../utils/cryptography/draft-EIP712.sol"; -import "../../../utils/cryptography/ECDSA.sol"; -import "../../../utils/Counters.sol"; - -/** - * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - * - * _Available since v3.4._ - */ -abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { - using Counters for Counters.Counter; - - mapping(address => Counters.Counter) private _nonces; - - // solhint-disable-next-line var-name-mixedcase - bytes32 private immutable _PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - - /** - * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. - * - * It's a good idea to use the same `name` that is defined as the ERC721 token name. - */ - constructor(string memory name) EIP712(name, "1") {} - - /** - * @dev See {IERC721Permit-permit}. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual override { - require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); - - bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); - - bytes32 hash = _hashTypedDataV4(structHash); - - address signer = ECDSA.recover(hash, v, r, s); - require(signer == owner, "ERC721Permit: invalid signature"); - - _approve(spender, value); - } - - /** - * @dev See {IERC721Permit-nonces}. - */ - function nonces(address owner) public view virtual override returns (uint256) { - return _nonces[owner].current(); - } - - /** - * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view override returns (bytes32) { - return _domainSeparatorV4(); - } - - /** - * @dev "Consume a nonce": return the current value and increment. - * - * _Available since v4.1._ - */ - function _useNonce(address owner) internal virtual returns (uint256 current) { - Counters.Counter storage nonce = _nonces[owner]; - current = nonce.current(); - nonce.increment(); - } -} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol deleted file mode 100644 index 61882f2de0d..00000000000 --- a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - */ -interface IERC721Permit { - /** - * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, - * given ``owner``'s signed approval. - * - * IMPORTANT: The same issues {IERC721-approve} has related to transaction - * ordering also apply here. - * - * Emits an {Approval} event. - * - * Requirements: - * - * - `spender` cannot be the zero address. - * - `deadline` must be a timestamp in the future. - * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` - * over the EIP712-formatted function arguments. - * - the signature must use ``owner``'s current nonce (see {nonces}). - * - * For more information on the signature format, see the - * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP - * section]. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - /** - * @dev Returns the current nonce for `owner`. This value must be - * included whenever a signature is generated for {permit}. - * - * Every successful call to {permit} increases ``owner``'s nonce by one. This - * prevents a signature from being used multiple times. - */ - function nonces(address owner) external view returns (uint256); - - /** - * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view returns (bytes32); -} diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 1a114c1c6b2..21d5587d66f 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -61,7 +61,7 @@ contract('ERC721Votes', function (accounts) { const name = 'My Token'; const symbol = 'MTKN'; const version = '1'; - const supply = new BN('10000000000000000000000000'); + const initalTokenId = new BN('10000000000000000000000000'); beforeEach(async function () { this.token = await ERC721VotesMock.new(name, symbol); @@ -89,7 +89,7 @@ contract('ERC721Votes', function (accounts) { this.token.mint(holder, NFT1); this.token.mint(holder, NFT2); this.token.mint(holder, NFT3); - this.token.mint(holder, supply); + this.token.mint(holder, initalTokenId); await expectRevert( this.token.mint(holder, lastTokenId), @@ -100,7 +100,7 @@ contract('ERC721Votes', function (accounts) { describe('set delegation', function () { describe('call', function () { it('delegation with tokenId', async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, initalTokenId); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -151,7 +151,7 @@ contract('ERC721Votes', function (accounts) { }}); beforeEach(async function () { - await this.token.mint(delegatorAddress, supply); + await this.token.mint(delegatorAddress, initalTokenId); }); it('accept signed delegation', async function () { @@ -257,7 +257,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, initalTokenId); await this.token.delegate(holder, { from: holder }); }); @@ -295,12 +295,12 @@ contract('ERC721Votes', function (accounts) { describe('transfers', function () { beforeEach(async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, initalTokenId); }); it('no delegation', async function () { - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -310,8 +310,8 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -324,8 +324,8 @@ contract('ERC721Votes', function (accounts) { it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -339,8 +339,8 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -366,7 +366,7 @@ contract('ERC721Votes', function (accounts) { // 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); + await this.token.mint(holder, initalTokenId); await this.token.mint(holder, NFT1); await this.token.mint(holder, NFT2); await this.token.mint(holder, NFT3); @@ -505,7 +505,7 @@ contract('ERC721Votes', function (accounts) { }); it('returns the latest block if >= last checkpoint block', async function () { - t1 = await this.token.mint(holder, supply); + t1 = await this.token.mint(holder, initalTokenId); await time.advanceBlock(); await time.advanceBlock(); @@ -516,7 +516,7 @@ contract('ERC721Votes', function (accounts) { it('returns zero if < first checkpoint block', async function () { await time.advanceBlock(); - const t1 = await this.token.mint(holder, supply); + const t1 = await this.token.mint(holder, initalTokenId); await time.advanceBlock(); await time.advanceBlock(); From 9fb0dd3a306e5de3a4c1caaea5d7a4e6651e451a Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 17:45:23 -0400 Subject: [PATCH 061/300] Updating tests based on new contract changes --- contracts/mocks/ERC721PermitMock.sol | 20 -------- contracts/mocks/ERC721VotesMock.sol | 2 +- .../token/ERC721/extensions/ERC721Votes.sol | 50 +++++++++++++++++-- .../ERC721/extensions/ERC721Votes.test.js | 34 ++++++------- 4 files changed, 63 insertions(+), 43 deletions(-) delete mode 100644 contracts/mocks/ERC721PermitMock.sol diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol deleted file mode 100644 index 37a860ef4dc..00000000000 --- a/contracts/mocks/ERC721PermitMock.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../token/ERC721/extensions/draft-ERC721Permit.sol"; - -contract ERC721PermitMock is ERC721Permit { - constructor( - string memory name, - string memory symbol, - address initialAccount, - uint256 tokenId - ) payable ERC721(name, symbol) ERC721Permit(name) { - _mint(initialAccount, tokenId); - } - - function getChainId() external view returns (uint256) { - return block.chainid; - } -} diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 09e40a4ea85..3b7a1bed7a8 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { - constructor(string memory name, string memory symbol) ERC721(name, symbol) ERC721Permit(name) {} + constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {} function mint(address account, uint256 tokenId) public { _mint(account, tokenId); diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 18164dc6721..00a5e8707c0 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -4,9 +4,11 @@ pragma solidity ^0.8.0; import "../ERC721.sol"; +import "../../../utils/Counters.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; /** * @dev Extension of ERC721 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. @@ -22,19 +24,29 @@ import "../../../utils/cryptography/ECDSA.sol"; * * _Available since v4.2._ */ -abstract contract ERC721Votes is ERC721 { +abstract contract ERC721Votes is ERC721, EIP712 { + using Counters for Counters.Counter; + struct Checkpoint { uint32 fromBlock; uint224 votes; } - + uint256 _totalSupply; bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); mapping(address => address) private _delegates; + mapping(address => Counters.Counter) private _nonces; mapping(address => Checkpoint[]) private _checkpoints; Checkpoint[] private _totalSupplyCheckpoints; + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC721 token name. + + constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ + /** * @dev Emitted when an account changes their delegate. */ @@ -169,7 +181,8 @@ abstract contract ERC721Votes is ERC721 { */ function _mint(address account, uint256 tokenId) internal virtual override { super._mint(account, tokenId); - require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + _totalSupply += 1; + require(_totalSupply <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } @@ -179,7 +192,7 @@ abstract contract ERC721Votes is ERC721 { */ function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); - + _totalSupply -= 1; _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); } @@ -190,7 +203,8 @@ abstract contract ERC721Votes is ERC721 { */ function _afterTokenTransfer( address from, - address to + address to, + uint256 tokenId ) internal virtual override{ _moveVotingPower(delegates(from), delegates(to), 1); } @@ -244,6 +258,32 @@ abstract contract ERC721Votes is ERC721 { } } + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } + + /** + * @dev Returns an address nonce. + */ + function nonces(address owner) public view virtual returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev Returns DOMAIN_SEPARATOR. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparatorV4(); + } + function _add(uint256 a, uint256 b) private pure returns (uint256) { return a + b; } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 21d5587d66f..a5b9cd16198 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -299,7 +299,7 @@ contract('ERC721Votes', function (accounts) { }); it('no delegation', async function () { - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); @@ -310,7 +310,7 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); @@ -324,7 +324,7 @@ contract('ERC721Votes', function (accounts) { it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -339,7 +339,7 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -381,20 +381,20 @@ contract('ERC721Votes', function (accounts) { describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { - await this.token.transferFrom(recipient, NFT1, { from: holder }); //give an account two tokens for readability - await this.token.transferFrom(recipient, NFT2, { from: holder }); + await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - const t2 = await this.token.transferFrom(other2, NFT1, { from: recipient }); + const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - const t3 = await this.token.transferFrom(other2, NFT2, { from: recipient }); + const t3 = await this.token.transferFrom(recipient, other2, NFT2, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); @@ -410,19 +410,19 @@ contract('ERC721Votes', function (accounts) { }); it('does not add more than one checkpoint in a block', async function () { - await this.token.transferFrom(recipient, NFT1, { from: holder }); - await this.token.transferFrom(recipient, NFT2, { from: holder }); + await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); + await this.token.transferFrom(holder, recipient, NFT2, { 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.transferFrom(other2, NFT1, { from: recipient, gas: 200000 }), - () => this.token.transferFrom(other2, NFT2, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(recipient, other2, NFT1, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(recipient, other2, NFT2, { from: recipient, gas: 200000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); - const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); @@ -465,13 +465,13 @@ contract('ERC721Votes', function (accounts) { const t1 = await this.token.delegate(other1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t2 = await this.token.transferFrom(other2, NFT1, { from: holder }); + const t2 = await this.token.transferFrom(holder, other2, NFT1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.transferFrom(other2, NFT2, { from: holder }); + const t3 = await this.token.transferFrom(holder, other2, NFT2, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transferFrom(holder, NFT2, { from: other2 }); + const t4 = await this.token.transferFrom(other2, holder, NFT2, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); From 8165b68d8f53e3256bdab6cb2487184e46caadcc Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 20:15:54 -0400 Subject: [PATCH 062/300] Updating execution order inside of mint --- contracts/token/ERC721/extensions/ERC721Votes.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 00a5e8707c0..7871282643b 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -180,10 +180,11 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been increased. */ function _mint(address account, uint256 tokenId) internal virtual override { + require(_totalSupply+1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + super._mint(account, tokenId); _totalSupply += 1; - require(_totalSupply <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } From 90ededdcea67cc9fff4befc7a417cbc27433f0bf Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 16:25:14 -0400 Subject: [PATCH 063/300] Adding Mocks for testing and integrating nft minting to current workflow test --- .../extensions/GovernorVotesERC721.sol | 2 +- contracts/mocks/GovernorERC721Mock.sol | 41 +++++++++ test/governance/GovernorWorkflow.behavior.js | 4 +- .../extensions/GovernorERC721.test.js | 91 +++++++++++++++++++ 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 contracts/mocks/GovernorERC721Mock.sol create mode 100644 test/governance/extensions/GovernorERC721.test.js diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 383960d9ef1..2e3079fc243 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -12,7 +12,7 @@ import "../../utils/math/Math.sol"; * * _Available since v4.3._ */ -abstract contract ERC721GovernorVotesERC721 is Governor { +abstract contract GovernorVotesERC721 is Governor { ERC721Votes public immutable token; constructor(ERC721Votes tokenAddress) { diff --git a/contracts/mocks/GovernorERC721Mock.sol b/contracts/mocks/GovernorERC721Mock.sol new file mode 100644 index 00000000000..7508f168334 --- /dev/null +++ b/contracts/mocks/GovernorERC721Mock.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../governance/extensions/GovernorCountingSimple.sol"; +import "../governance/extensions/GovernorVotesERC721.sol"; + +contract GovernorERC721Mock is GovernorVotesERC721, GovernorCountingSimple { + constructor(string memory name_, ERC721Votes token_) Governor(name_) GovernorVotesERC721(token_) {} + + 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; + } + + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 salt + ) public returns (uint256 proposalId) { + return _cancel(targets, values, calldatas, salt); + } + + function getVotes(address account, uint256 blockNumber) + public + view + virtual + override(IGovernor, GovernorVotesERC721) + returns (uint256) + { + return super.getVotes(account, blockNumber); + } +} diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js index 70319cd44d3..e8e2416b19e 100644 --- a/test/governance/GovernorWorkflow.behavior.js +++ b/test/governance/GovernorWorkflow.behavior.js @@ -31,7 +31,9 @@ function runGovernorWorkflow () { for (const voter of this.settings.voters) { if (voter.weight) { await this.token.transfer(voter.voter, voter.weight, { from: this.settings.tokenHolder }); - } + }else if(voter.nftWeight){ + await this.token.transferFrom(this.settings.tokenHolder, voter.voter, voter.nftWeight, { from: this.settings.tokenHolder }); + } } } diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js new file mode 100644 index 00000000000..845d5c20d21 --- /dev/null +++ b/test/governance/extensions/GovernorERC721.test.js @@ -0,0 +1,91 @@ +const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const Enums = require('../../helpers/enums'); + +const { + runGovernorWorkflow, +} = require('./../GovernorWorkflow.behavior'); + +const Token = artifacts.require('ERC721VotesMock'); +const Governor = artifacts.require('GovernorERC721Mock'); +const CallReceiver = artifacts.require('CallReceiverMock'); + +contract('GovernorERC721Mock', function (accounts) { + const [ owner, voter1, voter2, voter3, voter4 ] = accounts; + + const name = 'OZ-Governor'; + const tokenName = 'MockNFToken'; + const tokenSymbol = 'MTKN'; + const initalTokenId = web3.utils.toWei('100'); + const NFT1 = web3.utils.toWei('10'); + const NFT2 = web3.utils.toWei('20'); + const NFT3 = web3.utils.toWei('30'); + + beforeEach(async function () { + this.owner = owner; + this.token = await Token.new(tokenName, tokenSymbol); + this.mock = await Governor.new(name, this.token.address); + this.receiver = await CallReceiver.new(); + await this.token.mint(owner, initalTokenId); + await this.token.mint(owner, NFT1); + await this.token.mint(owner, NFT2); + await this.token.mint(owner, NFT3); + + await this.token.delegate(voter1, { from: voter1 }); + await this.token.delegate(voter2, { from: voter2 }); + await this.token.delegate(voter3, { from: voter3 }); + await this.token.delegate(voter4, { from: voter4 }); + }); + + 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('4'); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16'); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + }); + + describe.only('voting with ERC721 token', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, nftWeight: initalTokenId, support: Enums.VoteType.For }, + { voter: voter2, nftWeight: NFT1, support: Enums.VoteType.For }, + { voter: voter3, nftWeight: NFT2, support: Enums.VoteType.Against }, + { voter: voter4, nftWeight: NFT3, support: Enums.VoteType.Abstain }, + ] + } + }); + + afterEach(async function () { + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter3)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter4)).to.be.equal(true); + + this.receipts.castVote.filter(Boolean).forEach(vote => { + const { voter } = vote.logs.find(Boolean).args; + expectEvent( + vote, + 'VoteCast', + this.settings.voters.find(({ address }) => address === voter), + ); + }); + await this.mock.proposalVotes(this.id).then(result => { + for (const [key, value] of Object.entries(Enums.VoteType)) { + expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( + Object.values(this.settings.voters).filter(({ support }) => support === value).length.toString() + ); + } + }); + }); + runGovernorWorkflow(); + }); +}); From e9b1782b3046faeb79d333daf3b78a69786dffb1 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 17:02:56 -0400 Subject: [PATCH 064/300] Implementing override test --- .../extensions/GovernorERC721.test.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 845d5c20d21..4f81055800a 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -64,20 +64,22 @@ contract('GovernorERC721Mock', function (accounts) { }); afterEach(async function () { - expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(true); - expect(await this.mock.hasVoted(this.id, voter3)).to.be.equal(true); - expect(await this.mock.hasVoted(this.id, voter4)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); - this.receipts.castVote.filter(Boolean).forEach(vote => { + for(const vote of this.receipts.castVote.filter(Boolean)){ const { voter } = vote.logs.find(Boolean).args; + + expect(await this.mock.hasVoted(this.id, voter)).to.be.equal(true); + expectEvent( vote, 'VoteCast', this.settings.voters.find(({ address }) => address === voter), ); - }); + + expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); + } + await this.mock.proposalVotes(this.id).then(result => { for (const [key, value] of Object.entries(Enums.VoteType)) { expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( @@ -86,6 +88,8 @@ contract('GovernorERC721Mock', function (accounts) { } }); }); + runGovernorWorkflow(); + }); }); From 0788974e3bfa092f4e8fb1146c306b9abb271d51 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 17:47:26 -0400 Subject: [PATCH 065/300] Removing .only from tests --- test/governance/extensions/GovernorERC721.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 4f81055800a..ee8ad8399da 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -44,7 +44,7 @@ contract('GovernorERC721Mock', function (accounts) { expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); }); - describe.only('voting with ERC721 token', function () { + describe('voting with ERC721 token', function () { beforeEach(async function () { this.settings = { proposal: [ From 94ffadc2bde3bca0a5c8a4d73383d09e03d2cf8d Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 18:05:24 -0400 Subject: [PATCH 066/300] Updating contracts READMEs --- contracts/governance/README.adoc | 4 ++++ contracts/token/ERC721/README.adoc | 2 ++ 2 files changed, 6 insertions(+) diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index d388f4e3a42..ef5d416c012 100644 --- a/contracts/governance/README.adoc +++ b/contracts/governance/README.adoc @@ -22,6 +22,8 @@ Votes modules determine the source of voting power, and sometimes quorum number. * {GovernorVotes}: Extracts voting weight from an {ERC20Votes} token. +* {GovernorVotesERC721}: Extracts voting weight from 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. @@ -64,6 +66,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you {{GovernorVotesQuorumFraction}} +{{GovernorVotesERC721}} + {{GovernorVotesComp}} === Extensions diff --git a/contracts/token/ERC721/README.adoc b/contracts/token/ERC721/README.adoc index f1122c53a99..51089e1627c 100644 --- a/contracts/token/ERC721/README.adoc +++ b/contracts/token/ERC721/README.adoc @@ -41,6 +41,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel {{ERC721URIStorage}} +{{ERC721Votes}} + == Presets These contracts are preconfigured combinations of the above features. They can be used through inheritance or as models to copy and paste their source code. From e90f58e009c57618aeaac2b80cb9c427498379b0 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 18:57:08 -0400 Subject: [PATCH 067/300] Governance adocs update --- contracts/mocks/ERC721VotesMock.sol | 2 +- contracts/mocks/UserTOkenerc721Mock.sol | 32 +++++++++++++ .../token/ERC721/extensions/ERC721Votes.sol | 3 +- docs/modules/ROOT/pages/erc721.adoc | 2 +- docs/modules/ROOT/pages/governance.adoc | 45 ++++++++++++++++++- 5 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 3b7a1bed7a8..bde65e5a5ff 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { - constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {} + constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} function mint(address account, uint256 tokenId) public { _mint(account, tokenId); diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol new file mode 100644 index 00000000000..b3874e66da5 --- /dev/null +++ b/contracts/mocks/UserTOkenerc721Mock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; + +contract MyToken is ERC721, EIP712, ERC721Votes { + constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._burn(account, amount); + } +} \ No newline at end of file diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 7871282643b..39c03934028 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -44,8 +44,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. * * It's a good idea to use the same `name` that is defined as the ERC721 token name. - - constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ + */ /** * @dev Emitted when an account changes their delegate. diff --git a/docs/modules/ROOT/pages/erc721.adoc b/docs/modules/ROOT/pages/erc721.adoc index 8d28fad2e6e..14dbdc97606 100644 --- a/docs/modules/ROOT/pages/erc721.adoc +++ b/docs/modules/ROOT/pages/erc721.adoc @@ -1,6 +1,6 @@ = ERC721 -We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate* or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. +We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate*, *voting rights*, or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. ERC721 is a more complex standard than ERC20, with multiple optional extensions, and is split across a number of contracts. The OpenZeppelin Contracts provide flexibility regarding how these are combined, along with custom useful extensions. Check out the xref:api:token/ERC721.adoc[API Reference] to learn more about these. diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 027301c1ba5..a262a75af38 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -14,6 +14,10 @@ This governance protocol is generally implemented in a special-purpose contract OpenZeppelin’s Governor system was designed with a concern for compatibility with existing systems that were based on Compound’s GovernorAlpha and GovernorBravo. Because of this, you will find that many modules are presented in two variants, one of which is built for compatibility with those systems. +=== ERC721Votes + +The ERC721 extension to keep track of votes and vote delegation is one such case. + === ERC20Votes & ERC20VotesComp The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. @@ -119,11 +123,48 @@ contract MyToken is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper { } ``` +If your project requires The voting power of each account in our governance setup will be determined by an ERC721 token. The token has to implement the ERC721Votes extension. This extension will keep track of historical balances so that voting power is retrieved from past snapshots rather than current balance, which is an important protection that prevents double voting. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; + +contract MyToken is ERC721, EIP712, ERC721Votes { + constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._burn(account, amount); + } +} +``` + NOTE: Voting power could be determined in different ways: multiple ERC20 tokens, ERC721 tokens, sybil resistant identities, etc. All of these options are potentially supported by writing a custom Votes module for your Governor. === Governor -Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, and 3) what options people have when casting a vote and how those votes are counted. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. +Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, 3) what options people have when casting a vote and how those votes are counted, and 4)what type of token should be use to vote. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. For 1) we will use the GovernorVotes module, which hooks to an ERC20Votes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. @@ -131,6 +172,8 @@ For 2) we will use GovernorVotesQuorumFraction which works together with ERC20Vo For 3) we will use GovernorCountingSimple, a module that offers 3 options to voters: For, Against, and Abstain, and where only For and Abstain votes are counted towards quorum. +For 4) we will use the GovernorVotesERC721 module, which hooks to an ERC721Votes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. + Besides these modules, Governor itself has some parameters we must set. votingDelay: How long after a proposal is created should voting power be fixed. A large voting delay gives users time to unstake tokens if necessary. From c2cd9b1aa39f4156fe5c191652798655e483a105 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 14:13:44 -0400 Subject: [PATCH 068/300] Removing test contract --- contracts/mocks/UserTOkenerc721Mock.sol | 32 ------------------------- 1 file changed, 32 deletions(-) delete mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol deleted file mode 100644 index b3874e66da5..00000000000 --- a/contracts/mocks/UserTOkenerc721Mock.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "@openzeppelin/contracts/token/ERC721.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; - -contract MyToken is ERC721, EIP712, ERC721Votes { - constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._afterTokenTransfer(from, to, amount); - } - - function _mint(address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._mint(to, amount); - } - - function _burn(address account, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._burn(account, amount); - } -} \ No newline at end of file From ec2c44e104a75e25f98ffa32a96783691a430973 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sat, 30 Oct 2021 20:49:29 -0400 Subject: [PATCH 069/300] Initial contracts creation --- .../extensions/GovernorVotesERC721.sol | 2 +- .../ERC721/extensions/draft-ERC721Permit.sol | 87 +++++++++++++++++++ .../ERC721/extensions/draft-IERC721Permit.sol | 60 +++++++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol create mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 2e3079fc243..c2472a2e4e2 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC721GovernorVotesERC721.sol) +// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC72GovernorVotesERC721.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol new file mode 100644 index 00000000000..c00d15367ab --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) + +pragma solidity ^0.8.0; + +import "./draft-IERC721Permit.sol"; +import "./ERC721Enumerable.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; +import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/Counters.sol"; + +/** + * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * _Available since v3.4._ + */ +abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { + using Counters for Counters.Counter; + + mapping(address => Counters.Counter) private _nonces; + + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC721 token name. + */ + constructor(string memory name) EIP712(name, "1") {} + + /** + * @dev See {IERC721Permit-permit}. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); + + bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + + bytes32 hash = _hashTypedDataV4(structHash); + + address signer = ECDSA.recover(hash, v, r, s); + require(signer == owner, "ERC721Permit: invalid signature"); + + _approve(spender, value); + } + + /** + * @dev See {IERC721Permit-nonces}. + */ + function nonces(address owner) public view virtual override returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } +} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol new file mode 100644 index 00000000000..61882f2de0d --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + */ +interface IERC721Permit { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC721-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} From fe28ed9434e12b9c403576fbcc40c9f6ee076591 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 09:28:35 -0400 Subject: [PATCH 070/300] creating permit tests --- contracts/mocks/ERC721PermitMock.sol | 20 ++ .../ERC721/extensions/ERC721Votes.test.js | 233 +++++++++++++++++- .../extensions/draft-ERC721Permit.test.js | 117 +++++++++ 3 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 contracts/mocks/ERC721PermitMock.sol create mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol new file mode 100644 index 00000000000..37a860ef4dc --- /dev/null +++ b/contracts/mocks/ERC721PermitMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC721/extensions/draft-ERC721Permit.sol"; + +contract ERC721PermitMock is ERC721Permit { + constructor( + string memory name, + string memory symbol, + address initialAccount, + uint256 tokenId + ) payable ERC721(name, symbol) ERC721Permit(name) { + _mint(initialAccount, tokenId); + } + + function getChainId() external view returns (uint256) { + return block.chainid; + } +} diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index a5b9cd16198..3d925cc89b5 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -14,7 +14,6 @@ const queue = promisify(setImmediate); const ERC721VotesMock = artifacts.require('ERC721VotesMock'); const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); -const { Console } = require('console'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -55,6 +54,7 @@ async function batchInBlock (txs) { contract('ERC721Votes', function (accounts) { const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; +<<<<<<< HEAD const NFT1 = new BN('10'); const NFT2 = new BN('20'); const NFT3 = new BN('30'); @@ -62,6 +62,13 @@ contract('ERC721Votes', function (accounts) { const symbol = 'MTKN'; const version = '1'; const initalTokenId = new BN('10000000000000000000000000'); +======= + + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; + const supply = new BN('10000000000000000000000000'); +>>>>>>> creating permit tests beforeEach(async function () { this.token = await ERC721VotesMock.new(name, symbol); @@ -85,6 +92,7 @@ contract('ERC721Votes', function (accounts) { }); it('minting restriction', async function () { +<<<<<<< HEAD const lastTokenId = new BN('2').pow(new BN('224')); this.token.mint(holder, NFT1); this.token.mint(holder, NFT2); @@ -93,14 +101,24 @@ contract('ERC721Votes', function (accounts) { await expectRevert( this.token.mint(holder, lastTokenId), +======= + const amount = new BN('2').pow(new BN('224')); + await expectRevert( + this.token.mint(holder, amount), +>>>>>>> creating permit tests 'ERC721Votes: total supply risks overflowing votes', ); }); describe('set delegation', function () { describe('call', function () { +<<<<<<< HEAD it('delegation with tokenId', async function () { await this.token.mint(holder, initalTokenId); +======= + it('delegation with balance', async function () { + await this.token.mint(holder, supply); +>>>>>>> creating permit tests expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -112,11 +130,16 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '0', +<<<<<<< HEAD newBalance: '1', +======= + newBalance: supply, +>>>>>>> creating permit tests }); expect(await this.token.delegates(holder)).to.be.equal(holder); +<<<<<<< HEAD expect(await this.token.getVotes(holder)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); @@ -124,6 +147,15 @@ contract('ERC721Votes', function (accounts) { }); it('delegation without tokenId', async function () { +======= + expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); + }); + + it('delegation without balance', async function () { +>>>>>>> creating permit tests expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -151,7 +183,11 @@ contract('ERC721Votes', function (accounts) { }}); beforeEach(async function () { +<<<<<<< HEAD await this.token.mint(delegatorAddress, initalTokenId); +======= + await this.token.mint(delegatorAddress, supply); +>>>>>>> creating permit tests }); it('accept signed delegation', async function () { @@ -175,15 +211,26 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: delegatorAddress, previousBalance: '0', +<<<<<<< HEAD newBalance: '1', +======= + newBalance: supply, +>>>>>>> creating permit tests }); expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); +<<<<<<< HEAD expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1'); +======= + expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); +>>>>>>> creating permit tests }); it('rejects reused signature', async function () { @@ -257,7 +304,11 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { +<<<<<<< HEAD await this.token.mint(holder, initalTokenId); +======= + await this.token.mint(holder, supply); +>>>>>>> creating permit tests await this.token.delegate(holder, { from: holder }); }); @@ -272,35 +323,61 @@ contract('ERC721Votes', function (accounts) { }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, +<<<<<<< HEAD previousBalance: '1', +======= + previousBalance: supply, +>>>>>>> creating permit tests newBalance: '0', }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holderDelegatee, previousBalance: '0', +<<<<<<< HEAD newBalance: '1', +======= + newBalance: supply, +>>>>>>> creating permit tests }); expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); +<<<<<<< HEAD expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); +======= + expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); +>>>>>>> creating permit tests }); }); describe('transfers', function () { beforeEach(async function () { +<<<<<<< HEAD await this.token.mint(holder, initalTokenId); }); it('no delegation', async function () { const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); +======= + 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' }); +>>>>>>> creating permit tests expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -310,22 +387,37 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); +<<<<<<< HEAD const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); +======= + 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) }); +>>>>>>> creating permit tests const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); expect(receipt.logs.filter(({ event }) => event == 'DelegateVotesChanged').every(({ logIndex }) => transferLogIndex < logIndex)).to.be.equal(true); +<<<<<<< HEAD this.holderVotes = '0'; +======= + this.holderVotes = supply.subn(1); +>>>>>>> creating permit tests this.recipientVotes = '0'; }); it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); +<<<<<<< HEAD const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); +======= + const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); +>>>>>>> creating permit tests expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -339,15 +431,25 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); +<<<<<<< HEAD const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); +======= + 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) }); +>>>>>>> creating permit tests 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); +<<<<<<< HEAD this.holderVotes = '0'; +======= + this.holderVotes = supply.subn(1); +>>>>>>> creating permit tests this.recipientVotes = '1'; }); @@ -366,27 +468,40 @@ contract('ERC721Votes', function (accounts) { // 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 () { +<<<<<<< HEAD await this.token.mint(holder, initalTokenId); await this.token.mint(holder, NFT1); await this.token.mint(holder, NFT2); await this.token.mint(holder, NFT3); +======= + await this.token.mint(holder, supply); +>>>>>>> creating permit tests }); describe('balanceOf', function () { it('grants to initial account', async function () { +<<<<<<< HEAD expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('4'); +======= + expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); +>>>>>>> creating permit tests }); }); describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { +<<<<<<< HEAD await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); +======= + await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability +>>>>>>> creating permit tests expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); +<<<<<<< HEAD const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); @@ -425,6 +540,47 @@ contract('ERC721Votes', function (accounts) { const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); +======= + + const t2 = await this.token.transfer(other2, 10, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + + const t3 = await this.token.transfer(other2, 10, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); + + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '100' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '90' ]); + expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '80' ]); + expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + + await time.advanceBlock(); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); + }); + + it('does not add more than one checkpoint in a block', async function () { + await this.token.transfer(recipient, '100', { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const [ t1, t2, t3 ] = await batchInBlock([ + () => this.token.delegate(other1, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + ]); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); + // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check + // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); +>>>>>>> creating permit tests }); }); @@ -445,8 +601,13 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); +<<<<<<< HEAD expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('4'); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); +======= + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); +>>>>>>> creating permit tests }); it('returns zero if < first checkpoint block', async function () { @@ -456,6 +617,7 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); +<<<<<<< HEAD expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); }); @@ -484,6 +646,34 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3'); expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); +======= + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.transfer(holder, 20, { from: other2 }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); +>>>>>>> creating permit tests }); }); }); @@ -505,22 +695,36 @@ contract('ERC721Votes', function (accounts) { }); it('returns the latest block if >= last checkpoint block', async function () { +<<<<<<< HEAD t1 = await this.token.mint(holder, initalTokenId); +======= + t1 = await this.token.mint(holder, supply); +>>>>>>> creating permit tests await time.advanceBlock(); await time.advanceBlock(); +<<<<<<< HEAD expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); +======= + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); +>>>>>>> creating permit tests }); it('returns zero if < first checkpoint block', async function () { await time.advanceBlock(); +<<<<<<< HEAD const t1 = await this.token.mint(holder, initalTokenId); +======= + const t1 = await this.token.mint(holder, supply); +>>>>>>> creating permit tests await time.advanceBlock(); await time.advanceBlock(); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); +<<<<<<< HEAD expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); @@ -538,10 +742,27 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); const t5 = await this.token.mint(holder, NFT3); +======= + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.mint(holder, supply); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.burn(10); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.burn(10); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.mint(holder, 20); +>>>>>>> creating permit tests await time.advanceBlock(); await time.advanceBlock(); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); +<<<<<<< HEAD expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); @@ -552,6 +773,16 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); +======= + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); +>>>>>>> creating permit tests }); }); }); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js new file mode 100644 index 00000000000..7a7b8cf5d83 --- /dev/null +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -0,0 +1,117 @@ +/* eslint-disable */ + +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const ERC721PermitMock = artifacts.require('ERC721PermitMock'); + +const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); + +const Permit = [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, +]; + +contract('ERC721Permit', function (accounts) { + const [ initialHolder, spender, recipient, other ] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; + + const initialTokenId = new BN('100'); + + beforeEach(async function () { + this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); + + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = await this.token.getChainId(); + }); + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect( + await this.token.DOMAIN_SEPARATOR(), + ).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address), + ); + }); + + describe.only('permit', function () { + const wallet = Wallet.generate(); + + const owner = wallet.getAddressString(); + const value = initialTokenId; + const nonce = 0; + const maxDeadline = MAX_UINT256; + + const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ + primaryType: 'Permit', + types: { EIP712Domain, Permit }, + domain: { name, version, chainId, verifyingContract }, + message: { owner, spender, value, nonce, deadline }, + }); + + it('accepts owner signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); + expect(await this.token.getApproved(value)).to.be.equal(spender); + }); + + it('rejects reused signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects other signature', async function () { + const otherWallet = Wallet.generate(); + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects expired permit', async function () { + const deadline = (await time.latest()) - time.duration.weeks(1); + + const data = buildData(this.chainId, this.token.address, deadline); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, deadline, v, r, s), + 'ERC721Permit: expired deadline', + ); + }); + }); +}); From abeaf5c5e7012dd4b9a5a6b49789f9f1a0e5619c Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 10:26:22 -0400 Subject: [PATCH 071/300] Fixing checkpoints count --- .../token/ERC721/extensions/ERC721Votes.sol | 8 +- .../ERC721/extensions/ERC721Votes.test.js | 312 +++--------------- .../extensions/draft-ERC721Permit.test.js | 2 +- 3 files changed, 44 insertions(+), 278 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 39c03934028..6a7ef27dbec 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -8,7 +8,6 @@ import "../../../utils/Counters.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; -import "../../../utils/cryptography/draft-EIP712.sol"; /** * @dev Extension of ERC721 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. @@ -179,11 +178,9 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been increased. */ function _mint(address account, uint256 tokenId) internal virtual override { - require(_totalSupply+1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - super._mint(account, tokenId); - _totalSupply += 1; - + require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } @@ -192,7 +189,6 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); - _totalSupply -= 1; _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 3d925cc89b5..81397dda8aa 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -53,22 +53,15 @@ async function batchInBlock (txs) { } contract('ERC721Votes', function (accounts) { - const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; -<<<<<<< HEAD + const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; + const NFT0 = new BN('10000000000000000000000000'); const NFT1 = new BN('10'); const NFT2 = new BN('20'); - const NFT3 = new BN('30'); + const NFT3 = new BN('30'); + const NFT4 = new BN('40'); const name = 'My Token'; const symbol = 'MTKN'; const version = '1'; - const initalTokenId = new BN('10000000000000000000000000'); -======= - - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - const supply = new BN('10000000000000000000000000'); ->>>>>>> creating permit tests beforeEach(async function () { this.token = await ERC721VotesMock.new(name, symbol); @@ -92,33 +85,23 @@ contract('ERC721Votes', function (accounts) { }); it('minting restriction', async function () { -<<<<<<< HEAD const lastTokenId = new BN('2').pow(new BN('224')); this.token.mint(holder, NFT1); this.token.mint(holder, NFT2); this.token.mint(holder, NFT3); - this.token.mint(holder, initalTokenId); + this.token.mint(holder, NFT0); + this.token.mint(holder, NFT4); await expectRevert( this.token.mint(holder, lastTokenId), -======= - const amount = new BN('2').pow(new BN('224')); - await expectRevert( - this.token.mint(holder, amount), ->>>>>>> creating permit tests 'ERC721Votes: total supply risks overflowing votes', ); }); describe('set delegation', function () { describe('call', function () { -<<<<<<< HEAD - it('delegation with tokenId', async function () { - await this.token.mint(holder, initalTokenId); -======= - it('delegation with balance', async function () { - await this.token.mint(holder, supply); ->>>>>>> creating permit tests + it('delegation with tokens', async function () { + await this.token.mint(holder, NFT0); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -130,32 +113,18 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '0', -<<<<<<< HEAD newBalance: '1', -======= - newBalance: supply, ->>>>>>> creating permit tests }); expect(await this.token.delegates(holder)).to.be.equal(holder); -<<<<<<< HEAD expect(await this.token.getVotes(holder)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); }); - it('delegation without tokenId', async function () { -======= - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); - }); - - it('delegation without balance', async function () { ->>>>>>> creating permit tests + it('delegation without tokens', async function () { expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -183,11 +152,7 @@ contract('ERC721Votes', function (accounts) { }}); beforeEach(async function () { -<<<<<<< HEAD - await this.token.mint(delegatorAddress, initalTokenId); -======= - await this.token.mint(delegatorAddress, supply); ->>>>>>> creating permit tests + await this.token.mint(delegatorAddress, NFT0); }); it('accept signed delegation', async function () { @@ -211,26 +176,15 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: delegatorAddress, previousBalance: '0', -<<<<<<< HEAD newBalance: '1', -======= - newBalance: supply, ->>>>>>> creating permit tests }); expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); -<<<<<<< HEAD expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1'); -======= - expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); ->>>>>>> creating permit tests }); it('rejects reused signature', async function () { @@ -304,11 +258,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { -<<<<<<< HEAD - await this.token.mint(holder, initalTokenId); -======= - await this.token.mint(holder, supply); ->>>>>>> creating permit tests + await this.token.mint(holder, NFT0); await this.token.delegate(holder, { from: holder }); }); @@ -323,61 +273,35 @@ contract('ERC721Votes', function (accounts) { }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, -<<<<<<< HEAD previousBalance: '1', -======= - previousBalance: supply, ->>>>>>> creating permit tests newBalance: '0', }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holderDelegatee, previousBalance: '0', -<<<<<<< HEAD newBalance: '1', -======= - newBalance: supply, ->>>>>>> creating permit tests }); expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); -<<<<<<< HEAD expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); -======= - expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); ->>>>>>> creating permit tests }); }); describe('transfers', function () { beforeEach(async function () { -<<<<<<< HEAD - await this.token.mint(holder, initalTokenId); - }); - - it('no delegation', async function () { - const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); -======= - await this.token.mint(holder, supply); + await this.token.mint(holder, NFT0); }); it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); ->>>>>>> creating permit tests + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -387,37 +311,22 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); -<<<<<<< HEAD - const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); -======= - 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) }); ->>>>>>> creating permit tests const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); expect(receipt.logs.filter(({ event }) => event == 'DelegateVotesChanged').every(({ logIndex }) => transferLogIndex < logIndex)).to.be.equal(true); -<<<<<<< HEAD this.holderVotes = '0'; -======= - this.holderVotes = supply.subn(1); ->>>>>>> creating permit tests this.recipientVotes = '0'; }); it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); -<<<<<<< HEAD - const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); -======= - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); ->>>>>>> creating permit tests + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -431,25 +340,15 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); -<<<<<<< HEAD - const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); -======= - 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) }); ->>>>>>> creating permit tests 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); -<<<<<<< HEAD this.holderVotes = '0'; -======= - this.holderVotes = supply.subn(1); ->>>>>>> creating permit tests this.recipientVotes = '1'; }); @@ -468,40 +367,27 @@ contract('ERC721Votes', function (accounts) { // 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 () { -<<<<<<< HEAD - await this.token.mint(holder, initalTokenId); + await this.token.mint(holder, NFT0); await this.token.mint(holder, NFT1); await this.token.mint(holder, NFT2); await this.token.mint(holder, NFT3); -======= - await this.token.mint(holder, supply); ->>>>>>> creating permit tests }); describe('balanceOf', function () { it('grants to initial account', async function () { -<<<<<<< HEAD expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('4'); -======= - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); ->>>>>>> creating permit tests }); }); describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { -<<<<<<< HEAD await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); -======= - await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability ->>>>>>> creating permit tests expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); -<<<<<<< HEAD const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); @@ -540,47 +426,6 @@ contract('ERC721Votes', function (accounts) { const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); -======= - - const t2 = await this.token.transfer(other2, 10, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - - const t3 = await this.token.transfer(other2, 10, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '100' ]); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '90' ]); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '80' ]); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); - - await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); - }); - - it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, '100', { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - - const [ t1, t2, t3 ] = await batchInBlock([ - () => this.token.delegate(other1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - ]); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); - // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); ->>>>>>> creating permit tests }); }); @@ -601,13 +446,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); -<<<<<<< HEAD expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('4'); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); -======= - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); ->>>>>>> creating permit tests }); it('returns zero if < first checkpoint block', async function () { @@ -617,7 +457,6 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); -<<<<<<< HEAD expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); }); @@ -646,86 +485,44 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3'); expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); -======= - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 20, { from: other2 }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); ->>>>>>> creating permit tests }); }); }); - describe('getPastTotalSupply', function () { + describe('getPastVotingPower', 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), + this.token.getPastVotingPower(5e10), 'ERC721Votes: block not yet mined', ); }); it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(0)).to.be.bignumber.equal('0'); }); it('returns the latest block if >= last checkpoint block', async function () { -<<<<<<< HEAD - t1 = await this.token.mint(holder, initalTokenId); -======= - t1 = await this.token.mint(holder, supply); ->>>>>>> creating permit tests + t1 = await this.token.mint(holder, NFT0); await time.advanceBlock(); await time.advanceBlock(); -<<<<<<< HEAD - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); -======= - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); ->>>>>>> creating permit tests + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('returns zero if < first checkpoint block', async function () { await time.advanceBlock(); -<<<<<<< HEAD - const t1 = await this.token.mint(holder, initalTokenId); -======= - const t1 = await this.token.mint(holder, supply); ->>>>>>> creating permit tests + const t1 = await this.token.mint(holder, NFT0); await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); -<<<<<<< HEAD - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { @@ -742,47 +539,20 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); const t5 = await this.token.mint(holder, NFT3); -======= - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.mint(holder, supply); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.burn(10); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.burn(10); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.mint(holder, 20); ->>>>>>> creating permit tests await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); -<<<<<<< HEAD - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); -======= - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); ->>>>>>> creating permit tests + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); }); }); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js index 7a7b8cf5d83..f22c904c724 100644 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -50,7 +50,7 @@ contract('ERC721Permit', function (accounts) { ); }); - describe.only('permit', function () { + describe('permit', function () { const wallet = Wallet.generate(); const owner = wallet.getAddressString(); From 798baa03a04336e898ce54495c5f4e63b4cb5242 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:20:14 -0400 Subject: [PATCH 072/300] Updating ERC721Vote tests --- .../token/ERC721/extensions/ERC721Votes.sol | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 6a7ef27dbec..38285ed9832 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -199,9 +199,8 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _afterTokenTransfer( address from, - address to, - uint256 tokenId - ) internal virtual override{ + address to + ) internal virtual { _moveVotingPower(delegates(from), delegates(to), 1); } @@ -287,4 +286,17 @@ abstract contract ERC721Votes is ERC721, EIP712 { function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } + + /** + * @dev Moves token from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 tokenId) external returns (bool){ + _transfer(_msgSender(), recipient, tokenId); + _afterTokenTransfer(_msgSender(), recipient); + return true; + } } From 4f5a44d487bf42ab830215acf7038f0ce94aa7e8 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 16:30:31 -0400 Subject: [PATCH 073/300] Renaming supplied tokenId on tests --- .../ERC721/extensions/draft-ERC721Permit.sol | 87 ------------------- .../ERC721/extensions/draft-IERC721Permit.sol | 60 ------------- 2 files changed, 147 deletions(-) delete mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol delete mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol deleted file mode 100644 index c00d15367ab..00000000000 --- a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) - -pragma solidity ^0.8.0; - -import "./draft-IERC721Permit.sol"; -import "./ERC721Enumerable.sol"; -import "../../../utils/cryptography/draft-EIP712.sol"; -import "../../../utils/cryptography/ECDSA.sol"; -import "../../../utils/Counters.sol"; - -/** - * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - * - * _Available since v3.4._ - */ -abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { - using Counters for Counters.Counter; - - mapping(address => Counters.Counter) private _nonces; - - // solhint-disable-next-line var-name-mixedcase - bytes32 private immutable _PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - - /** - * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. - * - * It's a good idea to use the same `name` that is defined as the ERC721 token name. - */ - constructor(string memory name) EIP712(name, "1") {} - - /** - * @dev See {IERC721Permit-permit}. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual override { - require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); - - bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); - - bytes32 hash = _hashTypedDataV4(structHash); - - address signer = ECDSA.recover(hash, v, r, s); - require(signer == owner, "ERC721Permit: invalid signature"); - - _approve(spender, value); - } - - /** - * @dev See {IERC721Permit-nonces}. - */ - function nonces(address owner) public view virtual override returns (uint256) { - return _nonces[owner].current(); - } - - /** - * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view override returns (bytes32) { - return _domainSeparatorV4(); - } - - /** - * @dev "Consume a nonce": return the current value and increment. - * - * _Available since v4.1._ - */ - function _useNonce(address owner) internal virtual returns (uint256 current) { - Counters.Counter storage nonce = _nonces[owner]; - current = nonce.current(); - nonce.increment(); - } -} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol deleted file mode 100644 index 61882f2de0d..00000000000 --- a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - */ -interface IERC721Permit { - /** - * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, - * given ``owner``'s signed approval. - * - * IMPORTANT: The same issues {IERC721-approve} has related to transaction - * ordering also apply here. - * - * Emits an {Approval} event. - * - * Requirements: - * - * - `spender` cannot be the zero address. - * - `deadline` must be a timestamp in the future. - * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` - * over the EIP712-formatted function arguments. - * - the signature must use ``owner``'s current nonce (see {nonces}). - * - * For more information on the signature format, see the - * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP - * section]. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - /** - * @dev Returns the current nonce for `owner`. This value must be - * included whenever a signature is generated for {permit}. - * - * Every successful call to {permit} increases ``owner``'s nonce by one. This - * prevents a signature from being used multiple times. - */ - function nonces(address owner) external view returns (uint256); - - /** - * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view returns (bytes32); -} From 13d4a91b70127fa545a4fe330fe9207ee48ea838 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 17:45:23 -0400 Subject: [PATCH 074/300] Updating tests based on new contract changes --- contracts/mocks/ERC721PermitMock.sol | 20 ------------------- contracts/mocks/ERC721VotesMock.sol | 4 ++++ .../token/ERC721/extensions/ERC721Votes.sol | 19 +++++++++++++++++- 3 files changed, 22 insertions(+), 21 deletions(-) delete mode 100644 contracts/mocks/ERC721PermitMock.sol diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol deleted file mode 100644 index 37a860ef4dc..00000000000 --- a/contracts/mocks/ERC721PermitMock.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../token/ERC721/extensions/draft-ERC721Permit.sol"; - -contract ERC721PermitMock is ERC721Permit { - constructor( - string memory name, - string memory symbol, - address initialAccount, - uint256 tokenId - ) payable ERC721(name, symbol) ERC721Permit(name) { - _mint(initialAccount, tokenId); - } - - function getChainId() external view returns (uint256) { - return block.chainid; - } -} diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index bde65e5a5ff..869dc27d6e3 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,7 +5,11 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { +<<<<<<< HEAD constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} +======= + constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {} +>>>>>>> Updating tests based on new contract changes function mint(address account, uint256 tokenId) public { _mint(account, tokenId); diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 38285ed9832..1e4e9a308d8 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -8,6 +8,7 @@ import "../../../utils/Counters.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; /** * @dev Extension of ERC721 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. @@ -43,7 +44,12 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. * * It's a good idea to use the same `name` that is defined as the ERC721 token name. +<<<<<<< HEAD */ +======= + + constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ +>>>>>>> Updating tests based on new contract changes /** * @dev Emitted when an account changes their delegate. @@ -179,7 +185,8 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _mint(address account, uint256 tokenId) internal virtual override { super._mint(account, tokenId); - require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + _totalSupply += 1; + require(_totalSupply <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } @@ -189,6 +196,10 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); +<<<<<<< HEAD +======= + _totalSupply -= 1; +>>>>>>> Updating tests based on new contract changes _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); } @@ -199,8 +210,14 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _afterTokenTransfer( address from, +<<<<<<< HEAD address to ) internal virtual { +======= + address to, + uint256 tokenId + ) internal virtual override{ +>>>>>>> Updating tests based on new contract changes _moveVotingPower(delegates(from), delegates(to), 1); } From 6ae58d32a0355872593c9e1476da23569fbe266b Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 20:15:54 -0400 Subject: [PATCH 075/300] Updating execution order inside of mint --- contracts/token/ERC721/extensions/ERC721Votes.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 1e4e9a308d8..eaf65ef2839 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -184,10 +184,11 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been increased. */ function _mint(address account, uint256 tokenId) internal virtual override { + require(_totalSupply+1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + super._mint(account, tokenId); _totalSupply += 1; - require(_totalSupply <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } From c8abe20a3938808fa5a857238bce475560067c14 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 16:25:14 -0400 Subject: [PATCH 076/300] Adding Mocks for testing and integrating nft minting to current workflow test --- .../extensions/GovernorERC721.test.js | 55 +++++++++++++------ 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index ee8ad8399da..11a0f382b28 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -1,4 +1,5 @@ -const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); +const { BN } = require('bn.js'); const Enums = require('../../helpers/enums'); const { @@ -15,21 +16,35 @@ contract('GovernorERC721Mock', function (accounts) { const name = 'OZ-Governor'; const tokenName = 'MockNFToken'; const tokenSymbol = 'MTKN'; - const initalTokenId = web3.utils.toWei('100'); + const NFT0 = web3.utils.toWei('100'); const NFT1 = web3.utils.toWei('10'); const NFT2 = web3.utils.toWei('20'); const NFT3 = web3.utils.toWei('30'); + const NFT4 = web3.utils.toWei('40'); + + // Must be the same as in contract + const ProposalState = { + Pending: new BN('0'), + Active: new BN('1'), + Canceled: new BN('2'), + Defeated: new BN('3'), + Succeeded: new BN('4'), + Queued: new BN('5'), + Expired: new BN('6'), + Executed: new BN('7'), + }; beforeEach(async function () { this.owner = owner; this.token = await Token.new(tokenName, tokenSymbol); this.mock = await Governor.new(name, this.token.address); this.receiver = await CallReceiver.new(); - await this.token.mint(owner, initalTokenId); + await this.token.mint(owner, NFT0); await this.token.mint(owner, NFT1); await this.token.mint(owner, NFT2); await this.token.mint(owner, NFT3); - + await this.token.mint(owner, NFT4); + await this.token.delegate(voter1, { from: voter1 }); await this.token.delegate(voter2, { from: voter2 }); await this.token.delegate(voter3, { from: voter3 }); @@ -55,20 +70,20 @@ contract('GovernorERC721Mock', function (accounts) { ], tokenHolder: owner, voters: [ - { voter: voter1, nftWeight: initalTokenId, support: Enums.VoteType.For }, - { voter: voter2, nftWeight: NFT1, support: Enums.VoteType.For }, - { voter: voter3, nftWeight: NFT2, support: Enums.VoteType.Against }, - { voter: voter4, nftWeight: NFT3, support: Enums.VoteType.Abstain }, - ] - } + { voter: voter1, nfts: [NFT0], support: Enums.VoteType.For }, + { voter: voter2, nfts: [NFT1, NFT2], support: Enums.VoteType.For }, + { voter: voter3, nfts: [NFT3], support: Enums.VoteType.Against }, + { voter: voter4, nfts: [NFT4], support: Enums.VoteType.Abstain }, + ], + }; }); afterEach(async function () { - expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); - for(const vote of this.receipts.castVote.filter(Boolean)){ + for (const vote of this.receipts.castVote.filter(Boolean)) { const { voter } = vote.logs.find(Boolean).args; - + expect(await this.mock.hasVoted(this.id, voter)).to.be.equal(true); expectEvent( @@ -77,19 +92,27 @@ contract('GovernorERC721Mock', function (accounts) { this.settings.voters.find(({ address }) => address === voter), ); - expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); + if (voter === voter2) { + expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('2'); + } else { + expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); + } } await this.mock.proposalVotes(this.id).then(result => { for (const [key, value] of Object.entries(Enums.VoteType)) { expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( - Object.values(this.settings.voters).filter(({ support }) => support === value).length.toString() + Object.values(this.settings.voters).filter(({ support }) => support === value).reduce( + (acc, { nfts }) => acc.add(new BN(nfts.length)), + new BN('0'), + ), ); } }); + + expect(await this.mock.state(this.id)).to.be.bignumber.equal(ProposalState.Executed); }); runGovernorWorkflow(); - }); }); From a5d52d9b3b472342d3c99a0ce6a03e8b9ff6bbdc Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 18:57:08 -0400 Subject: [PATCH 077/300] Governance adocs update --- contracts/mocks/ERC721VotesMock.sol | 12 +++---- contracts/mocks/UserTOkenerc721Mock.sol | 32 +++++++++++++++++++ .../token/ERC721/extensions/ERC721Votes.sol | 4 +++ 3 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 869dc27d6e3..b47bfd8f24e 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -2,14 +2,10 @@ pragma solidity ^0.8.0; -import "../token/ERC721/extensions/ERC721Votes.sol"; +import "../token/ERC721/extensions/draft-ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { -<<<<<<< HEAD - constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} -======= - constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {} ->>>>>>> Updating tests based on new contract changes + constructor(string memory name, string memory symbol) ERC721Votes(name, symbol) {} function mint(address account, uint256 tokenId) public { _mint(account, tokenId); @@ -23,7 +19,7 @@ contract ERC721VotesMock is ERC721Votes { return block.chainid; } - function _maxSupply() internal pure override returns(uint224){ - return uint224(4); + function _maxSupply() internal pure override returns (uint224) { + return uint224(5); } } diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol new file mode 100644 index 00000000000..b3874e66da5 --- /dev/null +++ b/contracts/mocks/UserTOkenerc721Mock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; + +contract MyToken is ERC721, EIP712, ERC721Votes { + constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._burn(account, amount); + } +} \ No newline at end of file diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index eaf65ef2839..819cedba82d 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -44,12 +44,16 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. * * It's a good idea to use the same `name` that is defined as the ERC721 token name. +<<<<<<< HEAD <<<<<<< HEAD */ ======= constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ >>>>>>> Updating tests based on new contract changes +======= + */ +>>>>>>> Governance adocs update /** * @dev Emitted when an account changes their delegate. From a2ec66b561f24ed367319764796785fb973adbec Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 14:46:27 -0400 Subject: [PATCH 078/300] Updating contracts listing order --- docs/modules/ROOT/pages/governance.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index a262a75af38..6014f778dc1 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -14,14 +14,14 @@ This governance protocol is generally implemented in a special-purpose contract OpenZeppelin’s Governor system was designed with a concern for compatibility with existing systems that were based on Compound’s GovernorAlpha and GovernorBravo. Because of this, you will find that many modules are presented in two variants, one of which is built for compatibility with those systems. -=== ERC721Votes - -The ERC721 extension to keep track of votes and vote delegation is one such case. - === ERC20Votes & ERC20VotesComp The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. +=== ERC721Votes + +The ERC721 extension to keep track of votes and vote delegation is one such case. + === Governor & GovernorCompatibilityBravo An OpenZeppelin Governor contract is by default not interface-compatible with GovernorAlpha or Bravo, since some of the functions are different or missing, although it shares all of the same events. However, it’s possible to opt in to full compatibility by inheriting from the GovernorCompatibilityBravo module. The contract will be cheaper to deploy and use without this module. From d8147d45822a379a03b3e4ff990e04e7bfc690d6 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 15:02:56 -0400 Subject: [PATCH 079/300] Following lint suggestions --- test/governance/GovernorWorkflow.behavior.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js index e8e2416b19e..ae178d7c9de 100644 --- a/test/governance/GovernorWorkflow.behavior.js +++ b/test/governance/GovernorWorkflow.behavior.js @@ -31,9 +31,10 @@ function runGovernorWorkflow () { for (const voter of this.settings.voters) { if (voter.weight) { await this.token.transfer(voter.voter, voter.weight, { from: this.settings.tokenHolder }); - }else if(voter.nftWeight){ - await this.token.transferFrom(this.settings.tokenHolder, voter.voter, voter.nftWeight, { from: this.settings.tokenHolder }); - } + } else if (voter.nftWeight) { + await this.token.transferFrom(this.settings.tokenHolder, voter.voter, voter.nftWeight, + { from: this.settings.tokenHolder }); + } } } From 2b03bf910033f11784621b9f081037324abd75ac Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 15:18:09 -0400 Subject: [PATCH 080/300] Delete of test contract --- contracts/mocks/UserTOkenerc721Mock.sol | 32 ------------------------- 1 file changed, 32 deletions(-) delete mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol deleted file mode 100644 index b3874e66da5..00000000000 --- a/contracts/mocks/UserTOkenerc721Mock.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "@openzeppelin/contracts/token/ERC721.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; - -contract MyToken is ERC721, EIP712, ERC721Votes { - constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._afterTokenTransfer(from, to, amount); - } - - function _mint(address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._mint(to, amount); - } - - function _burn(address account, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._burn(account, amount); - } -} \ No newline at end of file From 69c6bb175fead02d0c4f8c4ced90f75387567fa9 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 24 Nov 2021 16:17:09 -0400 Subject: [PATCH 081/300] Delete unused test file --- .../extensions/draft-ERC721Permit.test.js | 117 ------------------ 1 file changed, 117 deletions(-) delete mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js deleted file mode 100644 index f22c904c724..00000000000 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable */ - -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); -const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; - -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; - -const ERC721PermitMock = artifacts.require('ERC721PermitMock'); - -const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); - -const Permit = [ - { name: 'owner', type: 'address' }, - { name: 'spender', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint256' }, -]; - -contract('ERC721Permit', function (accounts) { - const [ initialHolder, spender, recipient, other ] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - - const initialTokenId = new BN('100'); - - beforeEach(async function () { - this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); - - // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id - // from within the EVM as from the JSON RPC interface. - // See https://github.com/trufflesuite/ganache-core/issues/515 - this.chainId = await this.token.getChainId(); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect( - await this.token.DOMAIN_SEPARATOR(), - ).to.equal( - await domainSeparator(name, version, this.chainId, this.token.address), - ); - }); - - describe('permit', function () { - const wallet = Wallet.generate(); - - const owner = wallet.getAddressString(); - const value = initialTokenId; - const nonce = 0; - const maxDeadline = MAX_UINT256; - - const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ - primaryType: 'Permit', - types: { EIP712Domain, Permit }, - domain: { name, version, chainId, verifyingContract }, - message: { owner, spender, value, nonce, deadline }, - }); - - it('accepts owner signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); - expect(await this.token.getApproved(value)).to.be.equal(spender); - }); - - it('rejects reused signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects other signature', async function () { - const otherWallet = Wallet.generate(); - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects expired permit', async function () { - const deadline = (await time.latest()) - time.duration.weeks(1); - - const data = buildData(this.chainId, this.token.address, deadline); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, deadline, v, r, s), - 'ERC721Permit: expired deadline', - ); - }); - }); -}); From 339aa313e10de6a0558303a89002b3a3b520f93d Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 25 Nov 2021 09:41:19 -0400 Subject: [PATCH 082/300] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 009a2fa4c8d..13876ba85f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ * `Math`: add a `abs(int256)` method that returns the unsigned absolute value of a signed value. ([#2984](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2984)) ## Unreleased - + * `Voting`: Create library to be use for ERC721 and ERC1155 voting ([#2944](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2944)) * `Ownable`: add an internal `_transferOwnership(address)`. ([#2568](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2568)) * `AccessControl`: add internal `_grantRole(bytes32,address)` and `_revokeRole(bytes32,address)`. ([#2568](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2568)) * `AccessControl`: mark `_setupRole(bytes32,address)` as deprecated in favor of `_grantRole(bytes32,address)`. ([#2568](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2568)) From 1cabd297b9ed722a8f6b8dab5ccf8184eb6b1e30 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 25 Nov 2021 09:54:14 -0400 Subject: [PATCH 083/300] Create Checkpoints and Voting libraries --- contracts/utils/Checkpoints.sol | 67 +++++++++++++++++ contracts/utils/Voting.sol | 126 ++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 contracts/utils/Checkpoints.sol create mode 100644 contracts/utils/Voting.sol diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol new file mode 100644 index 00000000000..408f4fc0d35 --- /dev/null +++ b/contracts/utils/Checkpoints.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./math/Math.sol"; +import "./math/SafeCast.sol"; + +library Checkpoints { + struct Checkpoint { + uint32 index; + uint224 value; + } + + struct History { + Checkpoint[] _checkpoints; + } + + function length(History storage self) internal view returns (uint256) { + return self._checkpoints.length; + } + + function at(History storage self, uint256 pos) internal view returns (Checkpoint memory) { + return self._checkpoints[pos]; + } + + function latest(History storage self) internal view returns (uint256) { + uint256 pos = length(self); + return pos == 0 ? 0 : at(self, pos - 1).value; + } + + function past(History storage self, uint256 index) internal view returns (uint256) { + require(index < block.number, "block not yet mined"); + + uint256 high = length(self); + uint256 low = 0; + while (low < high) { + uint256 mid = Math.average(low, high); + if (at(self, mid).index > index) { + high = mid; + } else { + low = mid + 1; + } + } + return high == 0 ? 0 : at(self, high - 1).value; + } + + function push( + History storage self, + uint256 value + ) internal returns (uint256, uint256) { + uint256 pos = length(self); + uint256 old = latest(self); + if (pos > 0 && self._checkpoints[pos - 1].index == block.number) { + self._checkpoints[pos - 1].value = SafeCast.toUint224(value); + } else { + self._checkpoints.push(Checkpoint({ index: SafeCast.toUint32(block.number), value: SafeCast.toUint224(value) })); + } + return (old, value); + } + + function push( + History storage self, + function(uint256, uint256) view returns (uint256) op, + uint256 delta + ) internal returns (uint256, uint256) { + return push(self, op(latest(self), delta)); + } +} diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol new file mode 100644 index 00000000000..b834e239a8f --- /dev/null +++ b/contracts/utils/Voting.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./Checkpoints.sol"; + +library Voting { + using Checkpoints for Checkpoints.History; + + struct Votes { + mapping(address => address) _delegation; + mapping(address => Checkpoints.History) _userCheckpoints; + Checkpoints.History _totalCheckpoints; + } + + function getVotes(Votes storage self, address account) internal view returns (uint256) { + return self._userCheckpoints[account].latest(); + } + + function getVotesAt(Votes storage self, address account, uint256 timestamp) internal view returns (uint256) { + return self._userCheckpoints[account].past(timestamp); + } + + function getTotalVotes(Votes storage self) internal view returns (uint256) { + return self._totalCheckpoints.latest(); + } + + function getTotalVotesAt(Votes storage self, uint256 timestamp) internal view returns (uint256) { + return self._totalCheckpoints.past(timestamp); + } + + function delegates(Votes storage self, address account) internal view returns (address) { + return self._delegation[account]; + } + + function delegate(Votes storage self, address account, address newDelegation, uint256 balance) internal { + address oldDelegation = delegates(self, account); + self._delegation[account] = newDelegation; + moveVotingPower(self, oldDelegation, newDelegation, balance, _dummy); + } + + function delegate( + Votes storage self, + address account, + address newDelegation, + uint256 balance, + function(address, uint256, uint256) hookDelegateVotesChanged + ) internal { + address oldDelegation = delegates(self, account); + self._delegation[account] = newDelegation; + moveVotingPower(self, oldDelegation, newDelegation, balance, hookDelegateVotesChanged); + } + + function mint(Votes storage self, address to, uint256 amount) internal { + self._totalCheckpoints.push(_add, amount); + moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); + } + + function mint( + Votes storage self, + address to, + uint256 amount, + function(address, uint256, uint256) hookDelegateVotesChanged + ) internal { + self._totalCheckpoints.push(_add, amount); + moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); + } + + function burn(Votes storage self, address from, uint256 amount) internal { + self._totalCheckpoints.push(_subtract, amount); + moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); + } + + function burn( + Votes storage self, + address from, + uint256 amount, + function(address, uint256, uint256) hookDelegateVotesChanged + ) internal { + self._totalCheckpoints.push(_subtract, amount); + moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); + } + + function transfer(Votes storage self, address from, address to, uint256 amount) internal { + moveVotingPower(self, delegates(self, from), delegates(self, to), amount, _dummy); + } + + function transfer( + Votes storage self, + address from, + address to, + uint256 amount, + function(address, uint256, uint256) hookDelegateVotesChanged + ) internal { + moveVotingPower(self, delegates(self, from), delegates(self, to), amount, hookDelegateVotesChanged); + } + + function _moveVotingPower( + Votes storage self, + address src, + address dst, + uint256 amount, + function(address, uint256, uint256) hookDelegateVotesChanged + ) private { + if (src != dst && amount > 0) { + if (src != address(0)) { + (uint256 oldValue, uint256 newValue) = self._userCheckpoints[src].push(_subtract, amount); + hookDelegateVotesChanged(src, oldValue, newValue); + } + if (dst != address(0)) { + (uint256 oldValue, uint256 newValue) = self._userCheckpoints[dst].push(_add, amount); + hookDelegateVotesChanged(dst, oldValue, newValue); + } + } + } + + function _add(uint256 a, uint256 b) private pure returns (uint256) { + return a + b; + } + + function _subtract(uint256 a, uint256 b) private pure returns (uint256) { + return a - b; + } + + function _dummy(address, uint256, uint256) private pure {} + +} From ec5ea0edb5e326d5f86e00ffb9e46658a88e618f Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 25 Nov 2021 16:40:27 -0400 Subject: [PATCH 084/300] Update ERC721Votes contract to use library --- .../ERC721/extensions/draft-ERC721Votes.sol | 116 +++-------------- contracts/utils/Voting.sol | 16 +-- .../extensions/draft-ERC721Permit.test.js | 117 ------------------ 3 files changed, 26 insertions(+), 223 deletions(-) delete mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index f25b6e2271f..13e4a43504e 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import "../ERC721.sol"; +import "../../../utils/Voting.sol"; import "../../../utils/Counters.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; @@ -27,19 +28,19 @@ import "../../../utils/cryptography/draft-EIP712.sol"; */ abstract contract ERC721Votes is ERC721, EIP712 { using Counters for Counters.Counter; + using Voting for Voting.Votes; struct Checkpoint { uint32 fromBlock; uint224 votes; } + uint256 _totalVotingPower; bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); - mapping(address => address) private _delegates; + Voting.Votes private _votes; mapping(address => Counters.Counter) private _nonces; - mapping(address => Checkpoint[]) private _checkpoints; - Checkpoint[] private _totalVotingPowerCheckpoints; /** * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. @@ -58,33 +59,25 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); - /** - * @dev Get the `pos`-th checkpoint for `account`. - */ - function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoint memory) { - return _checkpoints[account][pos]; - } - /** * @dev Get number of checkpoints for `account`. */ function numCheckpoints(address account) public view virtual returns (uint32) { - return SafeCast.toUint32(_checkpoints[account].length); + return SafeCast.toUint32(_votes.getVotes(account)); } /** * @dev Get the address `account` is currently delegating to. */ function delegates(address account) public view virtual returns (address) { - return _delegates[account]; + return _votes.delegates(account); } /** * @dev Gets the current votes balance for `account` */ function getVotes(address account) public view returns (uint256) { - uint256 pos = _checkpoints[account].length; - return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes; + return _votes.getVotes(account); } /** @@ -95,8 +88,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { * - `blockNumber` must have been already mined */ function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) { - require(blockNumber < block.number, "ERC721Votes: block not yet mined"); - return _checkpointsLookup(_checkpoints[account], blockNumber); + return _votes.getVotesAt(account, blockNumber); } /** @@ -109,38 +101,9 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function getPastVotingPower(uint256 blockNumber) public view returns (uint256) { require(blockNumber < block.number, "ERC721Votes: block not yet mined"); - return _checkpointsLookup(_totalVotingPowerCheckpoints, blockNumber); + return _votes.getTotalVotesAt(blockNumber); } - - /** - * @dev Lookup a value in a list of (sorted) checkpoints. - */ - function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) private view returns (uint256) { - // We run a binary search to look for the earliest checkpoint taken after `blockNumber`. - // - // During the loop, the index of the wanted checkpoint remains in the range [low-1, high). - // With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant. - // - If the middle checkpoint is after `blockNumber`, we look in [low, mid) - // - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high) - // Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not - // out of bounds (in which case we're looking too far in the past and the result is 0). - // Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is - // past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out - // the same. - uint256 high = ckpts.length; - uint256 low = 0; - while (low < high) { - uint256 mid = Math.average(low, high); - if (ckpts[mid].fromBlock > blockNumber) { - high = mid; - } else { - low = mid + 1; - } - } - - return high == 0 ? 0 : ckpts[high - 1].votes; - } - + /** * @dev Delegate votes from the sender to `delegatee`. */ @@ -186,7 +149,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { super._mint(account, tokenId); _totalVotingPower += 1; - _writeCheckpoint(_totalVotingPowerCheckpoints, _add, 1); + _votes.mint(account, 1, _hookDelegateVotesChanged); } /** @@ -195,7 +158,8 @@ abstract contract ERC721Votes is ERC721, EIP712 { function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); _totalVotingPower -= 1; - _writeCheckpoint(_totalVotingPowerCheckpoints, _subtract, 1); + address from = ownerOf(tokenId); + _votes.burn(from, 1, _hookDelegateVotesChanged); } /** @@ -209,8 +173,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { uint256 tokenId ) internal virtual override { super._afterTokenTransfer(from, to, tokenId); - - _moveVotingPower(delegates(from), delegates(to), 1); + _votes.transfer(from, to, 1, _hookDelegateVotesChanged); } /** @@ -219,47 +182,8 @@ abstract contract ERC721Votes is ERC721, EIP712 { * Emits events {DelegateChanged} and {DelegateVotesChanged}. */ function _delegate(address delegator, address delegatee) internal virtual { - address currentDelegate = delegates(delegator); - uint256 delegatorBalance = balanceOf(delegator); - _delegates[delegator] = delegatee; - - emit DelegateChanged(delegator, currentDelegate, delegatee); - - _moveVotingPower(currentDelegate, delegatee, delegatorBalance); - } - - function _moveVotingPower( - address src, - address dst, - uint256 amount - ) private { - if (src != dst && amount > 0) { - if (src != address(0)) { - (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[src], _subtract, amount); - emit DelegateVotesChanged(src, oldWeight, newWeight); - } - - if (dst != address(0)) { - (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[dst], _add, amount); - emit DelegateVotesChanged(dst, oldWeight, newWeight); - } - } - } - - function _writeCheckpoint( - Checkpoint[] storage ckpts, - function(uint256, uint256) view returns (uint256) op, - uint256 delta - ) private returns (uint256 oldWeight, uint256 newWeight) { - uint256 pos = ckpts.length; - oldWeight = pos == 0 ? 0 : ckpts[pos - 1].votes; - newWeight = op(oldWeight, delta); - - if (pos > 0 && ckpts[pos - 1].fromBlock == block.number) { - ckpts[pos - 1].votes = SafeCast.toUint224(newWeight); - } else { - ckpts.push(Checkpoint({fromBlock: SafeCast.toUint32(block.number), votes: SafeCast.toUint224(newWeight)})); - } + emit DelegateChanged(delegator, delegates(delegator), delegatee); + _votes.delegate(delegator, delegatee, balanceOf(delegator), _hookDelegateVotesChanged); } /** @@ -288,11 +212,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { return _domainSeparatorV4(); } - function _add(uint256 a, uint256 b) private pure returns (uint256) { - return a + b; - } - - function _subtract(uint256 a, uint256 b) private pure returns (uint256) { - return a - b; + function _hookDelegateVotesChanged(address account, uint256 previousBalance, uint256 newBalance) private { + emit DelegateVotesChanged(account, previousBalance, newBalance); } } diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol index b834e239a8f..e81e555798e 100644 --- a/contracts/utils/Voting.sol +++ b/contracts/utils/Voting.sol @@ -35,7 +35,7 @@ library Voting { function delegate(Votes storage self, address account, address newDelegation, uint256 balance) internal { address oldDelegation = delegates(self, account); self._delegation[account] = newDelegation; - moveVotingPower(self, oldDelegation, newDelegation, balance, _dummy); + _moveVotingPower(self, oldDelegation, newDelegation, balance, _dummy); } function delegate( @@ -47,12 +47,12 @@ library Voting { ) internal { address oldDelegation = delegates(self, account); self._delegation[account] = newDelegation; - moveVotingPower(self, oldDelegation, newDelegation, balance, hookDelegateVotesChanged); + _moveVotingPower(self, oldDelegation, newDelegation, balance, hookDelegateVotesChanged); } function mint(Votes storage self, address to, uint256 amount) internal { self._totalCheckpoints.push(_add, amount); - moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); + _moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); } function mint( @@ -62,12 +62,12 @@ library Voting { function(address, uint256, uint256) hookDelegateVotesChanged ) internal { self._totalCheckpoints.push(_add, amount); - moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); + _moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); } function burn(Votes storage self, address from, uint256 amount) internal { self._totalCheckpoints.push(_subtract, amount); - moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); + _moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); } function burn( @@ -77,11 +77,11 @@ library Voting { function(address, uint256, uint256) hookDelegateVotesChanged ) internal { self._totalCheckpoints.push(_subtract, amount); - moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); + _moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); } function transfer(Votes storage self, address from, address to, uint256 amount) internal { - moveVotingPower(self, delegates(self, from), delegates(self, to), amount, _dummy); + _moveVotingPower(self, delegates(self, from), delegates(self, to), amount, _dummy); } function transfer( @@ -91,7 +91,7 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) internal { - moveVotingPower(self, delegates(self, from), delegates(self, to), amount, hookDelegateVotesChanged); + _moveVotingPower(self, delegates(self, from), delegates(self, to), amount, hookDelegateVotesChanged); } function _moveVotingPower( diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js deleted file mode 100644 index f22c904c724..00000000000 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable */ - -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); -const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; - -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; - -const ERC721PermitMock = artifacts.require('ERC721PermitMock'); - -const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); - -const Permit = [ - { name: 'owner', type: 'address' }, - { name: 'spender', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint256' }, -]; - -contract('ERC721Permit', function (accounts) { - const [ initialHolder, spender, recipient, other ] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - - const initialTokenId = new BN('100'); - - beforeEach(async function () { - this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); - - // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id - // from within the EVM as from the JSON RPC interface. - // See https://github.com/trufflesuite/ganache-core/issues/515 - this.chainId = await this.token.getChainId(); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect( - await this.token.DOMAIN_SEPARATOR(), - ).to.equal( - await domainSeparator(name, version, this.chainId, this.token.address), - ); - }); - - describe('permit', function () { - const wallet = Wallet.generate(); - - const owner = wallet.getAddressString(); - const value = initialTokenId; - const nonce = 0; - const maxDeadline = MAX_UINT256; - - const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ - primaryType: 'Permit', - types: { EIP712Domain, Permit }, - domain: { name, version, chainId, verifyingContract }, - message: { owner, spender, value, nonce, deadline }, - }); - - it('accepts owner signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); - expect(await this.token.getApproved(value)).to.be.equal(spender); - }); - - it('rejects reused signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects other signature', async function () { - const otherWallet = Wallet.generate(); - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects expired permit', async function () { - const deadline = (await time.latest()) - time.duration.weeks(1); - - const data = buildData(this.chainId, this.token.address, deadline); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, deadline, v, r, s), - 'ERC721Permit: expired deadline', - ); - }); - }); -}); From 5f15d46c5889ef9e3cf83f9e9e333a208b029794 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 09:22:58 -0400 Subject: [PATCH 085/300] Add function to Voting library --- contracts/utils/Voting.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol index e81e555798e..963f28d17c2 100644 --- a/contracts/utils/Voting.sol +++ b/contracts/utils/Voting.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import "./Checkpoints.sol"; +import "hardhat/console.sol"; library Voting { using Checkpoints for Checkpoints.History; @@ -23,6 +24,10 @@ library Voting { function getTotalVotes(Votes storage self) internal view returns (uint256) { return self._totalCheckpoints.latest(); } + + function getTotalAccountVotes(Votes storage self, address account) internal view returns (uint256) { + return self._userCheckpoints[account].length(); + } function getTotalVotesAt(Votes storage self, uint256 timestamp) internal view returns (uint256) { return self._totalCheckpoints.past(timestamp); @@ -51,6 +56,7 @@ library Voting { } function mint(Votes storage self, address to, uint256 amount) internal { + console.log("minting 1"); self._totalCheckpoints.push(_add, amount); _moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); } @@ -61,11 +67,13 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) internal { + console.log("minting 2"); self._totalCheckpoints.push(_add, amount); _moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); } function burn(Votes storage self, address from, uint256 amount) internal { + console.log("burn 1"); self._totalCheckpoints.push(_subtract, amount); _moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); } @@ -76,6 +84,7 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) internal { + console.log("burn 2"); self._totalCheckpoints.push(_subtract, amount); _moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); } @@ -101,6 +110,8 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) private { + console.log("moving voting power"); + console.log(block.number); if (src != dst && amount > 0) { if (src != address(0)) { (uint256 oldValue, uint256 newValue) = self._userCheckpoints[src].push(_subtract, amount); From 82b15653d466210ecd4cd0f7ba0f82aa73bc0395 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 09:53:22 -0400 Subject: [PATCH 086/300] Update ERC721Governance contracts --- .../ERC721/extensions/draft-ERC721Votes.sol | 19 +++++++++------- contracts/utils/Voting.sol | 11 ++++------ .../ERC721/extensions/ERC721Votes.test.js | 22 +++++++++---------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 13e4a43504e..f09859dd613 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -7,6 +7,7 @@ import "../ERC721.sol"; import "../../../utils/Voting.sol"; import "../../../utils/Counters.sol"; import "../../../utils/math/Math.sol"; +import "../../../utils/Checkpoints.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; import "../../../utils/cryptography/draft-EIP712.sol"; @@ -30,11 +31,6 @@ abstract contract ERC721Votes is ERC721, EIP712 { using Counters for Counters.Counter; using Voting for Voting.Votes; - struct Checkpoint { - uint32 fromBlock; - uint224 votes; - } - uint256 _totalVotingPower; bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); @@ -63,7 +59,14 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Get number of checkpoints for `account`. */ function numCheckpoints(address account) public view virtual returns (uint32) { - return SafeCast.toUint32(_votes.getVotes(account)); + return SafeCast.toUint32(_votes.getTotalAccountVotes(account)); + } + + /** + * @dev Get the `pos`-th checkpoint for `account`. + */ + function checkpointAt(address account, uint32 pos) public view virtual returns (Checkpoints.Checkpoint memory) { + return _votes.getTotalAccountVotesAt(account, pos); } /** @@ -156,9 +159,9 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been decreased. */ function _burn(uint256 tokenId) internal virtual override { - super._burn(tokenId); - _totalVotingPower -= 1; address from = ownerOf(tokenId); + super._burn(tokenId); + _totalVotingPower -= 1; _votes.burn(from, 1, _hookDelegateVotesChanged); } diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol index 963f28d17c2..86541164b32 100644 --- a/contracts/utils/Voting.sol +++ b/contracts/utils/Voting.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; import "./Checkpoints.sol"; -import "hardhat/console.sol"; library Voting { using Checkpoints for Checkpoints.History; @@ -29,6 +28,10 @@ library Voting { return self._userCheckpoints[account].length(); } + function getTotalAccountVotesAt(Votes storage self, address account, uint32 pos) internal view returns (Checkpoints.Checkpoint memory) { + return self._userCheckpoints[account].at(pos); + } + function getTotalVotesAt(Votes storage self, uint256 timestamp) internal view returns (uint256) { return self._totalCheckpoints.past(timestamp); } @@ -56,7 +59,6 @@ library Voting { } function mint(Votes storage self, address to, uint256 amount) internal { - console.log("minting 1"); self._totalCheckpoints.push(_add, amount); _moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); } @@ -67,13 +69,11 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) internal { - console.log("minting 2"); self._totalCheckpoints.push(_add, amount); _moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); } function burn(Votes storage self, address from, uint256 amount) internal { - console.log("burn 1"); self._totalCheckpoints.push(_subtract, amount); _moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); } @@ -84,7 +84,6 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) internal { - console.log("burn 2"); self._totalCheckpoints.push(_subtract, amount); _moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); } @@ -110,8 +109,6 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) private { - console.log("moving voting power"); - console.log(block.number); if (src != dst && amount > 0) { if (src != address(0)) { (uint256 oldValue, uint256 newValue) = self._userCheckpoints[src].push(_subtract, amount); diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 81397dda8aa..0ef80342a7d 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -382,13 +382,12 @@ contract('ERC721Votes', function (accounts) { describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { - await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - + const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); @@ -398,10 +397,10 @@ contract('ERC721Votes', function (accounts) { const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); + expect(await this.token.checkpointAt(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); + expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); + expect(await this.token.checkpointAt(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); + expect(await this.token.checkpointAt(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); await time.advanceBlock(); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('2'); @@ -420,12 +419,13 @@ contract('ERC721Votes', function (accounts) { () => this.token.transferFrom(recipient, other2, NFT1, { from: recipient, gas: 200000 }), () => this.token.transferFrom(recipient, other2, NFT2, { from: recipient, gas: 200000 }), ]); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); + expect(await this.token.checkpointAt(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); + expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); }); @@ -433,7 +433,7 @@ contract('ERC721Votes', function (accounts) { it('reverts if block number >= current block', async function () { await expectRevert( this.token.getPastVotes(other1, 5e10), - 'ERC721Votes: block not yet mined', + 'block not yet mined', ); }); @@ -541,7 +541,7 @@ contract('ERC721Votes', function (accounts) { const t5 = await this.token.mint(holder, NFT3); await time.advanceBlock(); await time.advanceBlock(); - + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); From 8e3feaef8d771a91a92fa6f2654dc72180644079 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 11:30:11 -0400 Subject: [PATCH 087/300] Documentation for Checkpoints library --- contracts/utils/Checkpoints.sol | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index 408f4fc0d35..3e34f1f6933 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -4,6 +4,9 @@ pragma solidity ^0.8.0; import "./math/Math.sol"; import "./math/SafeCast.sol"; +/** + * @dev Checkpoints operations. + */ library Checkpoints { struct Checkpoint { uint32 index; @@ -14,19 +17,31 @@ library Checkpoints { Checkpoint[] _checkpoints; } + /** + * @dev Returns checkpoints length. + */ function length(History storage self) internal view returns (uint256) { return self._checkpoints.length; } + /** + * @dev Returns checkpoints at given position. + */ function at(History storage self, uint256 pos) internal view returns (Checkpoint memory) { return self._checkpoints[pos]; } + /** + * @dev Returns total amount of checkpoints. + */ function latest(History storage self) internal view returns (uint256) { uint256 pos = length(self); return pos == 0 ? 0 : at(self, pos - 1).value; } + /** + * @dev Returns checkpoints at given block number. + */ function past(History storage self, uint256 index) internal view returns (uint256) { require(index < block.number, "block not yet mined"); @@ -43,6 +58,9 @@ library Checkpoints { return high == 0 ? 0 : at(self, high - 1).value; } + /** + * @dev Creates checkpoint + */ function push( History storage self, uint256 value @@ -57,6 +75,9 @@ library Checkpoints { return (old, value); } + /** + * @dev Creates checkpoint + */ function push( History storage self, function(uint256, uint256) view returns (uint256) op, From d3f35023c985c3c431c01a2e3249cf9383d6b421 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 11:37:12 -0400 Subject: [PATCH 088/300] Add documentation for Voting library --- contracts/utils/Voting.sol | 65 +++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol index 86541164b32..caec09662f8 100644 --- a/contracts/utils/Voting.sol +++ b/contracts/utils/Voting.sol @@ -3,6 +3,9 @@ pragma solidity ^0.8.0; import "./Checkpoints.sol"; +/** + * @dev Voting operations. + */ library Voting { using Checkpoints for Checkpoints.History; @@ -12,40 +15,67 @@ library Voting { Checkpoints.History _totalCheckpoints; } + /** + * @dev Returns total amount of votes for account. + */ function getVotes(Votes storage self, address account) internal view returns (uint256) { return self._userCheckpoints[account].latest(); } + /** + * @dev Returns total amount of votes at given position. + */ function getVotesAt(Votes storage self, address account, uint256 timestamp) internal view returns (uint256) { return self._userCheckpoints[account].past(timestamp); } + /** + * @dev Get checkpoint for `account` for specific position. + */ + function getTotalAccountVotesAt(Votes storage self, address account, uint32 pos) internal view returns (Checkpoints.Checkpoint memory) { + return self._userCheckpoints[account].at(pos); + } + + /** + * @dev Returns total amount of votes. + */ function getTotalVotes(Votes storage self) internal view returns (uint256) { return self._totalCheckpoints.latest(); } + /** + * @dev Get number of checkpoints for `account`. + */ function getTotalAccountVotes(Votes storage self, address account) internal view returns (uint256) { return self._userCheckpoints[account].length(); } - function getTotalAccountVotesAt(Votes storage self, address account, uint32 pos) internal view returns (Checkpoints.Checkpoint memory) { - return self._userCheckpoints[account].at(pos); - } - + /** + * @dev Returns all votes for timestamp. + */ function getTotalVotesAt(Votes storage self, uint256 timestamp) internal view returns (uint256) { return self._totalCheckpoints.past(timestamp); } + /** + * @dev Updates delegation information. + */ function delegates(Votes storage self, address account) internal view returns (address) { return self._delegation[account]; } + /** + * @dev Delegates voting power. + */ function delegate(Votes storage self, address account, address newDelegation, uint256 balance) internal { address oldDelegation = delegates(self, account); self._delegation[account] = newDelegation; _moveVotingPower(self, oldDelegation, newDelegation, balance, _dummy); } + /** + * @dev Delegates voting power. + */ function delegate( Votes storage self, address account, @@ -58,11 +88,17 @@ library Voting { _moveVotingPower(self, oldDelegation, newDelegation, balance, hookDelegateVotesChanged); } + /** + * @dev Mints new vote. + */ function mint(Votes storage self, address to, uint256 amount) internal { self._totalCheckpoints.push(_add, amount); _moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); } + /** + * @dev Mints new vote. + */ function mint( Votes storage self, address to, @@ -73,11 +109,17 @@ library Voting { _moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); } + /** + * @dev Burns new vote. + */ function burn(Votes storage self, address from, uint256 amount) internal { self._totalCheckpoints.push(_subtract, amount); _moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); } + /** + * @dev Burns new vote. + */ function burn( Votes storage self, address from, @@ -88,10 +130,16 @@ library Voting { _moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); } + /** + * @dev Transfers voting power. + */ function transfer(Votes storage self, address from, address to, uint256 amount) internal { _moveVotingPower(self, delegates(self, from), delegates(self, to), amount, _dummy); } + /** + * @dev Transfers voting power. + */ function transfer( Votes storage self, address from, @@ -102,6 +150,9 @@ library Voting { _moveVotingPower(self, delegates(self, from), delegates(self, to), amount, hookDelegateVotesChanged); } + /** + * @dev Moves voting power. + */ function _moveVotingPower( Votes storage self, address src, @@ -121,10 +172,16 @@ library Voting { } } + /** + * @dev Adds two numbers. + */ function _add(uint256 a, uint256 b) private pure returns (uint256) { return a + b; } + /** + * @dev Subtracts two numbers. + */ function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } From 1b95cc879079e1799b673103839c3a2df5546362 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 12:06:34 -0400 Subject: [PATCH 089/300] Add Mock contracts for Voting and checkpoints test --- contracts/mocks/CheckpointsImpl.sol | 31 ++++++++++++++++ contracts/mocks/VotingImpl.sol | 56 +++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 contracts/mocks/CheckpointsImpl.sol create mode 100644 contracts/mocks/VotingImpl.sol diff --git a/contracts/mocks/CheckpointsImpl.sol b/contracts/mocks/CheckpointsImpl.sol new file mode 100644 index 00000000000..e5e0f92cd6c --- /dev/null +++ b/contracts/mocks/CheckpointsImpl.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/Checkpoints.sol"; + +contract CheckpointsImpl { + using Checkpoints for Checkpoints.History; + + Checkpoints.History private _totalCheckpoints; + + function length() public view returns (uint256) { + return _totalCheckpoints.length(); + } + + function at(uint256 pos) public view returns (Checkpoints.Checkpoint memory) { + return _totalCheckpoints.at(pos); + } + + function latest() public view returns (uint256) { + return _totalCheckpoints.latest(); + } + + function past(uint256 index) public view returns (uint256) { + return _totalCheckpoints.past(index); + } + + function push(uint256 value) public returns (uint256, uint256) { + return _totalCheckpoints.push(value); + } +} diff --git a/contracts/mocks/VotingImpl.sol b/contracts/mocks/VotingImpl.sol new file mode 100644 index 00000000000..6dd7990b97c --- /dev/null +++ b/contracts/mocks/VotingImpl.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/Voting.sol"; + +contract VotingImpl { + using Voting for Voting.Votes; + + Voting.Votes private _votes; + + function getVotes(address account) public view returns (uint256) { + return _votes.getVotes(account); + } + + function getVotesAt(address account, uint256 timestamp) public view returns (uint256) { + return _votes.getVotesAt(account, timestamp); + } + + function getTotalAccountVotesAt(address account, uint32 pos) public view returns (Checkpoints.Checkpoint memory) { + return _votes.getTotalAccountVotesAt(account, pos); + } + + function getTotalVotes() public view returns (uint256) { + return _votes.getTotalVotes(); + } + + function getTotalAccountVotes(address account) public view returns (uint256) { + return _votes.getTotalAccountVotes(account); + } + + function getTotalVotesAt(uint256 timestamp) public view returns (uint256) { + return _votes.getTotalVotesAt(timestamp); + } + + function delegates(address account) public view returns (address) { + return _votes.delegates(account); + } + + function delegate(address account, address newDelegation, uint256 balance) public { + return _votes.delegate(account, newDelegation, balance); + } + + function mint(address to, uint256 amount) public { + return _votes.mint(to, amount); + } + + function burn(address from, uint256 amount) public { + return _votes.burn(from, amount); + } + + function transfer(address from, address to, uint256 amount) public { + return _votes.transfer(from, to, amount); + } + +} From 9335d040e5c490898d198733b85829d96c037dc2 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 15:03:10 -0400 Subject: [PATCH 090/300] Add Voting library tests --- contracts/utils/Voting.sol | 4 +-- test/utils/Voting.test.js | 59 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 test/utils/Voting.test.js diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol index caec09662f8..0f33096c92c 100644 --- a/contracts/utils/Voting.sol +++ b/contracts/utils/Voting.sol @@ -35,7 +35,7 @@ library Voting { function getTotalAccountVotesAt(Votes storage self, address account, uint32 pos) internal view returns (Checkpoints.Checkpoint memory) { return self._userCheckpoints[account].at(pos); } - + /** * @dev Returns total amount of votes. */ @@ -58,7 +58,7 @@ library Voting { } /** - * @dev Updates delegation information. + * @dev Returns account delegation. */ function delegates(Votes storage self, address account) internal view returns (address) { return self._delegation[account]; diff --git a/test/utils/Voting.test.js b/test/utils/Voting.test.js new file mode 100644 index 00000000000..7f883886179 --- /dev/null +++ b/test/utils/Voting.test.js @@ -0,0 +1,59 @@ +const { expectRevert } = require('@openzeppelin/test-helpers'); + +const { expect } = require('chai'); + +const VotingImp = artifacts.require('VotingImpl'); + +contract('Voting', function (accounts) { + const [ account1, account2, account3 ] = accounts; + beforeEach(async function () { + this.voting = await VotingImp.new(); + }); + + it('starts with zero votes', async function () { + expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('0'); + }); + + describe('move voting power', function () { + + beforeEach(async function () { + this.tx1 = await this.voting.mint(account1,1); + this.tx2 = await this.voting.mint(account2,1); + this.tx3 = await this.voting.mint(account3,1); + }); + + it('mints', async function () { + expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('3'); + + expect(await this.voting.getTotalVotesAt(this.tx1.receipt.blockNumber-1)).to.be.bignumber.equal('0'); + expect(await this.voting.getTotalVotesAt(this.tx2.receipt.blockNumber-1)).to.be.bignumber.equal('1'); + expect(await this.voting.getTotalVotesAt(this.tx3.receipt.blockNumber-1)).to.be.bignumber.equal('2'); + }); + + it('burns', async function () { + await this.voting.burn(account1,1); + expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('2'); + + await this.voting.burn(account2,1); + expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('1'); + + await this.voting.burn(account3,1); + expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('0'); + }); + + it('delegates', async function () { + await this.voting.delegate(account3, account2, 1); + + expect(await this.voting.delegates(account3)).to.be.equal(account2); + }); + + it('transfers', async function () { + await this.voting.delegate(account1, account2, 1); + await this.voting.transfer(account1, account2, 1); + + expect(await this.voting.getTotalAccountVotes(account1)).to.be.bignumber.equal('0'); + expect(await this.voting.getTotalAccountVotes(account2)).to.be.bignumber.equal('2'); + }); + }); + +}); From c88fd74ffe18a4fdb2c743b7abf2d35c740abab4 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 15:34:18 -0400 Subject: [PATCH 091/300] Add Checkpoints library tests --- test/utils/Checkpoints.test.js | 42 ++++++++++++++++++++++++++++++++++ test/utils/Voting.test.js | 7 ++++++ 2 files changed, 49 insertions(+) create mode 100644 test/utils/Checkpoints.test.js diff --git a/test/utils/Checkpoints.test.js b/test/utils/Checkpoints.test.js new file mode 100644 index 00000000000..dbca463ccdf --- /dev/null +++ b/test/utils/Checkpoints.test.js @@ -0,0 +1,42 @@ +const { expectRevert } = require('@openzeppelin/test-helpers'); + +const { expect } = require('chai'); + +const CheckpointsImpl = artifacts.require('CheckpointsImpl'); + +contract('Checkpoints', function (accounts) { + beforeEach(async function () { + this.checkpoint = await CheckpointsImpl.new(); + + this.tx1 = await this.checkpoint.push(1); + this.tx2 = await this.checkpoint.push(2); + this.tx3 = await this.checkpoint.push(3); + }); + + it('calls length', async function () { + expect(await this.checkpoint.length()).to.be.bignumber.equal('3'); + }); + + it('calls at', async function () { + expect((await this.checkpoint.at(0))[1]).to.be.bignumber.equal('1'); + expect((await this.checkpoint.at(1))[1]).to.be.bignumber.equal('2'); + expect((await this.checkpoint.at(2))[1]).to.be.bignumber.equal('3'); + }); + + it('calls latest', async function () { + expect(await this.checkpoint.latest()).to.be.bignumber.equal('3'); + }); + + it('calls past', async function () { + expect(await this.checkpoint.past(this.tx1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.checkpoint.past(this.tx2.receipt.blockNumber - 1)).to.be.bignumber.equal('1'); + expect(await this.checkpoint.past(this.tx3.receipt.blockNumber - 1)).to.be.bignumber.equal('2'); + }); + + it('reverts if block number >= current block', async function () { + await expectRevert( + this.checkpoint.past(this.tx3.receipt.blockNumber + 1), + 'block not yet mined', + ); + }); +}); diff --git a/test/utils/Voting.test.js b/test/utils/Voting.test.js index 7f883886179..8e3ecb481d7 100644 --- a/test/utils/Voting.test.js +++ b/test/utils/Voting.test.js @@ -30,6 +30,13 @@ contract('Voting', function (accounts) { expect(await this.voting.getTotalVotesAt(this.tx3.receipt.blockNumber-1)).to.be.bignumber.equal('2'); }); + it('reverts if block number >= current block', async function () { + await expectRevert( + this.voting.getTotalVotesAt(this.tx3.receipt.blockNumber + 1), + 'block not yet mined', + ); + }); + it('burns', async function () { await this.voting.burn(account1,1); expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('2'); From b2c056807937c0d031e149c16c0d2cc0b033d070 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 15:49:33 -0400 Subject: [PATCH 092/300] Add more documentation for voting library --- contracts/utils/Voting.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol index 0f33096c92c..260b90f45d0 100644 --- a/contracts/utils/Voting.sol +++ b/contracts/utils/Voting.sol @@ -44,7 +44,7 @@ library Voting { } /** - * @dev Get number of checkpoints for `account`. + * @dev Get number of checkpoints for `account` including delegation. */ function getTotalAccountVotes(Votes storage self, address account) internal view returns (uint256) { return self._userCheckpoints[account].length(); From a0a26ce9663da3f037eabe05b676749af57f6a36 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sat, 30 Oct 2021 20:49:29 -0400 Subject: [PATCH 093/300] Initial contracts creation --- .../extensions/GovernorVotesERC721.sol | 28 ++ .../token/ERC721/extensions/ERC721Votes.sol | 258 ++++++++++++++++++ .../ERC721/extensions/draft-ERC721Permit.sol | 87 ++++++ .../ERC721/extensions/draft-IERC721Permit.sol | 60 ++++ 4 files changed, 433 insertions(+) create mode 100644 contracts/governance/extensions/GovernorVotesERC721.sol create mode 100644 contracts/token/ERC721/extensions/ERC721Votes.sol create mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol create mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol new file mode 100644 index 00000000000..9a2a4ac0ee8 --- /dev/null +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC72GovernorVotesERC721.sol) + +pragma solidity ^0.8.0; + +import "../Governor.sol"; +import "../../token/ERC721/extensions/ERC721Votes.sol"; +import "../../utils/math/Math.sol"; + +/** + * @dev Extension of {Governor} for voting weight extraction from an {ERC721Votes} token. + * + * _Available since v4.3._ + */ +abstract contract ERC72GovernorVotesERC721 is Governor { + ERC721Votes public immutable token; + + constructor(ERC721Votes tokenAddress) { + token = tokenAddress; + } + + /** + * Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). + */ + function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { + return token.getPastVotes(account, blockNumber); + } +} diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol new file mode 100644 index 00000000000..20da35f32a6 --- /dev/null +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/ERC721Votes.sol) + +pragma solidity ^0.8.0; + +import "./draft-ERC721Permit.sol"; +import "../../../utils/math/Math.sol"; +import "../../../utils/math/SafeCast.sol"; +import "../../../utils/cryptography/ECDSA.sol"; + +/** + * @dev Extension of ERC721 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 {ERC721VotesComp} 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 {getVotes} and {getPastVotes}. + * + * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it + * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. + * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this + * will significantly increase the base gas cost of transfers. + * + * _Available since v4.2._ + */ +abstract contract ERC721Votes is ERC721Permit { + struct Checkpoint { + uint32 fromBlock; + uint224 votes; + } + + bytes32 private constant _DELEGATION_TYPEHASH = + keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + + mapping(address => address) private _delegates; + mapping(address => Checkpoint[]) private _checkpoints; + Checkpoint[] private _totalSupplyCheckpoints; + + /** + * @dev Emitted when an account changes their delegate. + */ + event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); + + /** + * @dev Emitted when a token transfer or delegate change results in changes to an account's voting power. + */ + event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); + + /** + * @dev Get the `pos`-th checkpoint for `account`. + */ + function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoint memory) { + return _checkpoints[account][pos]; + } + + /** + * @dev Get number of checkpoints for `account`. + */ + function numCheckpoints(address account) public view virtual returns (uint32) { + return SafeCast.toUint32(_checkpoints[account].length); + } + + /** + * @dev Get the address `account` is currently delegating to. + */ + function delegates(address account) public view virtual returns (address) { + return _delegates[account]; + } + + /** + * @dev Gets the current votes balance for `account` + */ + function getVotes(address account) public view returns (uint256) { + uint256 pos = _checkpoints[account].length; + return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes; + } + + /** + * @dev Retrieve the number of votes for `account` at the end of `blockNumber`. + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) { + require(blockNumber < block.number, "ERC721Votes: block not yet mined"); + return _checkpointsLookup(_checkpoints[account], blockNumber); + } + + /** + * @dev Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances. + * It is but NOT the sum of all the delegated votes! + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastTotalSupply(uint256 blockNumber) public view returns (uint256) { + require(blockNumber < block.number, "ERC721Votes: block not yet mined"); + return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber); + } + + /** + * @dev Lookup a value in a list of (sorted) checkpoints. + */ + function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) private view returns (uint256) { + // We run a binary search to look for the earliest checkpoint taken after `blockNumber`. + // + // During the loop, the index of the wanted checkpoint remains in the range [low-1, high). + // With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant. + // - If the middle checkpoint is after `blockNumber`, we look in [low, mid) + // - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high) + // Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not + // out of bounds (in which case we're looking too far in the past and the result is 0). + // Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is + // past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out + // the same. + uint256 high = ckpts.length; + uint256 low = 0; + while (low < high) { + uint256 mid = Math.average(low, high); + if (ckpts[mid].fromBlock > blockNumber) { + high = mid; + } else { + low = mid + 1; + } + } + + return high == 0 ? 0 : ckpts[high - 1].votes; + } + + /** + * @dev Delegate votes from the sender to `delegatee`. + */ + function delegate(address delegatee) public virtual { + _delegate(_msgSender(), delegatee); + } + + /** + * @dev Delegates votes from signer to `delegatee` + */ + function delegateBySig( + address delegatee, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + require(block.timestamp <= expiry, "ERC721Votes: signature expired"); + address signer = ECDSA.recover( + _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), + v, + r, + s + ); + require(nonce == _useNonce(signer), "ERC721Votes: invalid nonce"); + _delegate(signer, delegatee); + } + + /** + * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). + */ + function _maxSupply() internal view virtual returns (uint224) { + return type(uint224).max; + } + + /** + * @dev Snapshots the totalSupply after it has been increased. + */ + function _mint(address account, uint256 amount) internal virtual override { + super._mint(account, amount);//TODO: update for NFT + require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + + _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); + } + + /** + * @dev Snapshots the totalSupply after it has been decreased. + */ + function _burn(uint256 tokenId) internal virtual override { + super._burn(tokenId); + + _writeCheckpoint(_totalSupplyCheckpoints, _subtract, tokenId); + } + + /** + * @dev Move voting power when tokens are transferred. + * + * Emits a {DelegateVotesChanged} event. + */ + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual { + _moveVotingPower(delegates(from), delegates(to), amount);//TODO: Update to be NFT logic + } + + /** + * @dev Change delegation for `delegator` to `delegatee`. + * + * Emits events {DelegateChanged} and {DelegateVotesChanged}. + */ + function _delegate(address delegator, address delegatee) internal virtual { + address currentDelegate = delegates(delegator); + uint256 delegatorBalance = balanceOf(delegator); + _delegates[delegator] = delegatee; + + emit DelegateChanged(delegator, currentDelegate, delegatee); + + _moveVotingPower(currentDelegate, delegatee, delegatorBalance); + } + + function _moveVotingPower( + address src, + address dst, + uint256 amount + ) private { + if (src != dst && amount > 0) { + if (src != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[src], _subtract, amount); + emit DelegateVotesChanged(src, oldWeight, newWeight); + } + + if (dst != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[dst], _add, amount); + emit DelegateVotesChanged(dst, oldWeight, newWeight); + } + } + } + + function _writeCheckpoint(//TODO: update for NFT + Checkpoint[] storage ckpts, + function(uint256, uint256) view returns (uint256) op, + uint256 delta + ) private returns (uint256 oldWeight, uint256 newWeight) { + uint256 pos = ckpts.length; + oldWeight = pos == 0 ? 0 : ckpts[pos - 1].votes; + newWeight = op(oldWeight, delta); + + if (pos > 0 && ckpts[pos - 1].fromBlock == block.number) { + ckpts[pos - 1].votes = SafeCast.toUint224(newWeight); + } else { + ckpts.push(Checkpoint({fromBlock: SafeCast.toUint32(block.number), votes: SafeCast.toUint224(newWeight)})); + } + } + + function _add(uint256 a, uint256 b) private pure returns (uint256) { + return a + b; + } + + function _subtract(uint256 a, uint256 b) private pure returns (uint256) { + return a - b; + } +} diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol new file mode 100644 index 00000000000..c00d15367ab --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) + +pragma solidity ^0.8.0; + +import "./draft-IERC721Permit.sol"; +import "./ERC721Enumerable.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; +import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/Counters.sol"; + +/** + * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * _Available since v3.4._ + */ +abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { + using Counters for Counters.Counter; + + mapping(address => Counters.Counter) private _nonces; + + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC721 token name. + */ + constructor(string memory name) EIP712(name, "1") {} + + /** + * @dev See {IERC721Permit-permit}. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); + + bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + + bytes32 hash = _hashTypedDataV4(structHash); + + address signer = ECDSA.recover(hash, v, r, s); + require(signer == owner, "ERC721Permit: invalid signature"); + + _approve(spender, value); + } + + /** + * @dev See {IERC721Permit-nonces}. + */ + function nonces(address owner) public view virtual override returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } +} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol new file mode 100644 index 00000000000..61882f2de0d --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + */ +interface IERC721Permit { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC721-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} From 0991757d3bcb0f0036150df01c4aee79644d7595 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 09:28:35 -0400 Subject: [PATCH 094/300] creating permit tests --- contracts/mocks/ERC721PermitMock.sol | 20 + contracts/mocks/ERC721VotesMock.sol | 21 + .../ERC721/extensions/ERC721Votes.test.js | 538 ++++++++++++++++++ .../extensions/draft-ERC721Permit.test.js | 117 ++++ 4 files changed, 696 insertions(+) create mode 100644 contracts/mocks/ERC721PermitMock.sol create mode 100644 contracts/mocks/ERC721VotesMock.sol create mode 100644 test/token/ERC721/extensions/ERC721Votes.test.js create mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol new file mode 100644 index 00000000000..37a860ef4dc --- /dev/null +++ b/contracts/mocks/ERC721PermitMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC721/extensions/draft-ERC721Permit.sol"; + +contract ERC721PermitMock is ERC721Permit { + constructor( + string memory name, + string memory symbol, + address initialAccount, + uint256 tokenId + ) payable ERC721(name, symbol) ERC721Permit(name) { + _mint(initialAccount, tokenId); + } + + function getChainId() external view returns (uint256) { + return block.chainid; + } +} diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol new file mode 100644 index 00000000000..457d05eedb9 --- /dev/null +++ b/contracts/mocks/ERC721VotesMock.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC721/extensions/ERC721Votes.sol"; + +contract ERC721VotesMock is ERC721Votes { + constructor(string memory name, string memory symbol) ERC721(name, symbol) ERC721Permit(name) {} + + function mint(address account, uint256 tokenId) public { + _mint(account, tokenId); + } + + function burn(uint256 tokenId) public { + _burn(tokenId); + } + + function getChainId() external view returns (uint256) { + return block.chainid; + } +} diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js new file mode 100644 index 00000000000..7078828039d --- /dev/null +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -0,0 +1,538 @@ +/* eslint-disable */ + +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const { promisify } = require('util'); +const queue = promisify(setImmediate); + +const ERC721VotesMock = artifacts.require('ERC721VotesMock'); + +const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); + +const Delegation = [ + { name: 'delegatee', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'expiry', type: 'uint256' }, +]; + +async function countPendingTransactions() { + return parseInt( + await network.provider.send('eth_getBlockTransactionCountByNumber', ['pending']) + ); +} + +async function batchInBlock (txs) { + try { + // disable auto-mining + await network.provider.send('evm_setAutomine', [false]); + // send all transactions + const promises = txs.map(fn => fn()); + // wait for node to have all pending transactions + while (txs.length > await countPendingTransactions()) { + await queue(); + } + // mine one block + await network.provider.send('evm_mine'); + // fetch receipts + const receipts = await Promise.all(promises); + // Sanity check, all tx should be in the same block + const minedBlocks = new Set(receipts.map(({ receipt }) => receipt.blockNumber)); + expect(minedBlocks.size).to.equal(1); + + return receipts; + } finally { + // enable auto-mining + await network.provider.send('evm_setAutomine', [true]); + } +} + +contract('ERC721Votes', function (accounts) { + const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; + const supply = new BN('10000000000000000000000000'); + + beforeEach(async function () { + this.token = await ERC721VotesMock.new(name, symbol); + + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = await this.token.getChainId(); + }); + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect( + await this.token.DOMAIN_SEPARATOR(), + ).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address), + ); + }); + + it('minting restriction', async function () { + const amount = new BN('2').pow(new BN('224')); + await expectRevert( + this.token.mint(holder, amount), + 'ERC721Votes: total supply risks overflowing votes', + ); + }); + + describe('set delegation', function () { + describe('call', function () { + it('delegation with balance', async function () { + await this.token.mint(holder, supply); + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + + expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); + }); + + it('delegation without balance', async function () { + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + }); + }); + + describe('with signature', function () { + const delegator = Wallet.generate(); + const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); + const nonce = 0; + + const buildData = (chainId, verifyingContract, message) => ({ data: { + primaryType: 'Delegation', + types: { EIP712Domain, Delegation }, + domain: { name, version, chainId, verifyingContract }, + message, + }}); + + beforeEach(async function () { + await this.token.mint(delegatorAddress, supply); + }); + + it('accept signed delegation', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + expectEvent(receipt, 'DelegateChanged', { + delegator: delegatorAddress, + fromDelegate: ZERO_ADDRESS, + toDelegate: delegatorAddress, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: delegatorAddress, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); + + expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); + }); + + it('rejects reused signature', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), + 'ERC721Votes: invalid nonce', + ); + }); + + it('rejects bad delegatee', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + const { logs } = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); + const { args } = logs.find(({ event }) => event == 'DelegateChanged'); + expect(args.delegator).to.not.be.equal(delegatorAddress); + expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); + expect(args.toDelegate).to.be.equal(holderDelegatee); + }); + + it('rejects bad nonce', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), + 'ERC721Votes: invalid nonce', + ); + }); + + it('rejects expired permit', async function () { + const expiry = (await time.latest()) - time.duration.weeks(1); + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry, + }), + )); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), + 'ERC721Votes: signature expired', + ); + }); + }); + }); + + describe('change delegation', function () { + beforeEach(async function () { + await this.token.mint(holder, supply); + await this.token.delegate(holder, { from: holder }); + }); + + it('call', async function () { + expect(await this.token.delegates(holder)).to.be.equal(holder); + + const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: holder, + toDelegate: holderDelegatee, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: supply, + newBalance: '0', + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holderDelegatee, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); + + expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); + expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); + }); + }); + + 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.getVotes(holder)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); + + // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" + const blockNumber = await time.latestBlock(); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getPastVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes); + }); + }); + + // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js. + describe('Compound test suite', function () { + beforeEach(async function () { + await this.token.mint(holder, supply); + }); + + describe('balanceOf', function () { + it('grants to initial account', async function () { + expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); + }); + }); + + describe('numCheckpoints', function () { + it('returns the number of checkpoints for a delegate', async function () { + await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const t1 = await this.token.delegate(other1, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + + const t2 = await this.token.transfer(other2, 10, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + + const t3 = await this.token.transfer(other2, 10, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); + + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '100' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '90' ]); + expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '80' ]); + expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + + await time.advanceBlock(); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); + }); + + it('does not add more than one checkpoint in a block', async function () { + await this.token.transfer(recipient, '100', { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const [ t1, t2, t3 ] = await batchInBlock([ + () => this.token.delegate(other1, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + ]); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); + // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check + // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + }); + }); + + describe('getPastVotes', function () { + it('reverts if block number >= current block', async function () { + await expectRevert( + this.token.getPastVotes(other1, 5e10), + 'ERC721Votes: block not yet mined', + ); + }); + + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.transfer(holder, 20, { from: other2 }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + }); + }); + + describe('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), + 'ERC721Votes: block not yet mined', + ); + }); + + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + t1 = await this.token.mint(holder, supply); + + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const t1 = await this.token.mint(holder, supply); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.mint(holder, supply); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.burn(10); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.burn(10); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.mint(holder, 20); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + }); +}); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js new file mode 100644 index 00000000000..7a7b8cf5d83 --- /dev/null +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -0,0 +1,117 @@ +/* eslint-disable */ + +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const ERC721PermitMock = artifacts.require('ERC721PermitMock'); + +const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); + +const Permit = [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, +]; + +contract('ERC721Permit', function (accounts) { + const [ initialHolder, spender, recipient, other ] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; + + const initialTokenId = new BN('100'); + + beforeEach(async function () { + this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); + + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = await this.token.getChainId(); + }); + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect( + await this.token.DOMAIN_SEPARATOR(), + ).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address), + ); + }); + + describe.only('permit', function () { + const wallet = Wallet.generate(); + + const owner = wallet.getAddressString(); + const value = initialTokenId; + const nonce = 0; + const maxDeadline = MAX_UINT256; + + const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ + primaryType: 'Permit', + types: { EIP712Domain, Permit }, + domain: { name, version, chainId, verifyingContract }, + message: { owner, spender, value, nonce, deadline }, + }); + + it('accepts owner signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); + expect(await this.token.getApproved(value)).to.be.equal(spender); + }); + + it('rejects reused signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects other signature', async function () { + const otherWallet = Wallet.generate(); + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects expired permit', async function () { + const deadline = (await time.latest()) - time.duration.weeks(1); + + const data = buildData(this.chainId, this.token.address, deadline); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, deadline, v, r, s), + 'ERC721Permit: expired deadline', + ); + }); + }); +}); From 9c3f9fde7ebfd7a2b49b8b0fd9ca74ac0c334027 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 10:26:22 -0400 Subject: [PATCH 095/300] Fixing checkpoints count --- .../token/ERC721/extensions/ERC721Votes.sol | 11 ++--- .../ERC721/extensions/ERC721Votes.test.js | 47 ++++++++++++------- .../extensions/draft-ERC721Permit.test.js | 2 +- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 20da35f32a6..b184c9c83fa 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -7,7 +7,6 @@ import "./draft-ERC721Permit.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; - /** * @dev Extension of ERC721 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. @@ -170,11 +169,11 @@ abstract contract ERC721Votes is ERC721Permit { /** * @dev Snapshots the totalSupply after it has been increased. */ - function _mint(address account, uint256 amount) internal virtual override { - super._mint(account, amount);//TODO: update for NFT + function _mint(address account, uint256 tokenId) internal virtual override { + super._mint(account, tokenId); require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } /** @@ -183,7 +182,7 @@ abstract contract ERC721Votes is ERC721Permit { function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); - _writeCheckpoint(_totalSupplyCheckpoints, _subtract, tokenId); + _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); } /** @@ -232,7 +231,7 @@ abstract contract ERC721Votes is ERC721Permit { } } - function _writeCheckpoint(//TODO: update for NFT + function _writeCheckpoint( Checkpoint[] storage ckpts, function(uint256, uint256) view returns (uint256) op, uint256 delta diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 7078828039d..d2981dd5f9d 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -54,7 +54,9 @@ async function batchInBlock (txs) { contract('ERC721Votes', function (accounts) { const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; - + const NFT1 = new BN('10'); + const NFT2 = new BN('20'); + const NFT3 = new BN('30'); const name = 'My Token'; const symbol = 'MTKN'; const version = '1'; @@ -91,7 +93,7 @@ contract('ERC721Votes', function (accounts) { describe('set delegation', function () { describe('call', function () { - it('delegation with balance', async function () { + it('delegation with balance', async function () {//TODO: Make it NFT like await this.token.mint(holder, supply); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); @@ -249,7 +251,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, supply);//TODO: Avoid tokenId duplicate await this.token.delegate(holder, { from: holder }); }); @@ -359,6 +361,9 @@ contract('ERC721Votes', function (accounts) { describe('Compound test suite', function () { beforeEach(async function () { await this.token.mint(holder, supply); + await this.token.mint(holder, NFT1); + await this.token.mint(holder, NFT2); + await this.token.mint(holder, NFT3); }); describe('balanceOf', function () { @@ -448,16 +453,16 @@ contract('ERC721Votes', function (accounts) { }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); + const t1 = await this.token.delegate(other1, { from: holder });//TODO: Make it NFT like 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 }); + const t3 = await this.token.transfer(other2, 20, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 20, { from: other2 }); + const t4 = await this.token.transfer(holder, 30, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); @@ -511,28 +516,34 @@ contract('ERC721Votes', function (accounts) { }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.mint(holder, supply); + const t1 = await this.token.mint(holder, NFT1); await time.advanceBlock(); await time.advanceBlock(); - const t2 = await this.token.burn(10); + const t2 = await this.token.burn(NFT1); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.burn(10); + const t3 = await this.token.mint(holder, NFT2); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.mint(holder, 20); + const t4 = await this.token.burn(NFT2); + await time.advanceBlock(); + await time.advanceBlock(); + const t5 = await this.token.mint(holder, NFT3); await time.advanceBlock(); await time.advanceBlock(); + console.log(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); }); }); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js index 7a7b8cf5d83..f22c904c724 100644 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -50,7 +50,7 @@ contract('ERC721Permit', function (accounts) { ); }); - describe.only('permit', function () { + describe('permit', function () { const wallet = Wallet.generate(); const owner = wallet.getAddressString(); From 0b0406e265c89eb61473ce8d32dca97483c96d87 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:20:14 -0400 Subject: [PATCH 096/300] Updating ERC721Vote tests --- contracts/mocks/ERC721VotesMock.sol | 4 + .../token/ERC721/extensions/ERC721Votes.sol | 18 ++- .../ERC721/extensions/ERC721Votes.test.js | 146 +++++++++--------- 3 files changed, 96 insertions(+), 72 deletions(-) diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 457d05eedb9..09e40a4ea85 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -18,4 +18,8 @@ contract ERC721VotesMock is ERC721Votes { function getChainId() external view returns (uint256) { return block.chainid; } + + function _maxSupply() internal pure override returns(uint224){ + return uint224(4); + } } diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index b184c9c83fa..146ff1dd213 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -192,10 +192,9 @@ abstract contract ERC721Votes is ERC721Permit { */ function _afterTokenTransfer( address from, - address to, - uint256 amount + address to ) internal virtual { - _moveVotingPower(delegates(from), delegates(to), amount);//TODO: Update to be NFT logic + _moveVotingPower(delegates(from), delegates(to), 1); } /** @@ -254,4 +253,17 @@ abstract contract ERC721Votes is ERC721Permit { function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } + + /** + * @dev Moves token from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 tokenId) external returns (bool){ + _transfer(_msgSender(), recipient, tokenId); + _afterTokenTransfer(_msgSender(), recipient); + return true; + } } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index d2981dd5f9d..b2d56ea7ab0 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -14,6 +14,7 @@ const queue = promisify(setImmediate); const ERC721VotesMock = artifacts.require('ERC721VotesMock'); const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); +const { Console } = require('console'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -84,9 +85,14 @@ contract('ERC721Votes', function (accounts) { }); it('minting restriction', async function () { - const amount = new BN('2').pow(new BN('224')); + const lastTokenId = new BN('2').pow(new BN('224')); + this.token.mint(holder, NFT1); + this.token.mint(holder, NFT2); + this.token.mint(holder, NFT3); + this.token.mint(holder, supply); + await expectRevert( - this.token.mint(holder, amount), + this.token.mint(holder, lastTokenId), 'ERC721Votes: total supply risks overflowing votes', ); }); @@ -106,15 +112,15 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '0', - newBalance: supply, + newBalance: '1', }); expect(await this.token.delegates(holder)).to.be.equal(holder); - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); + expect(await this.token.getVotes(holder)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); }); it('delegation without balance', async function () { @@ -169,15 +175,15 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: delegatorAddress, previousBalance: '0', - newBalance: supply, + newBalance: '1', }); expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); - expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); + expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1'); }); it('rejects reused signature', async function () { @@ -251,7 +257,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { - await this.token.mint(holder, supply);//TODO: Avoid tokenId duplicate + await this.token.mint(holder, supply); await this.token.delegate(holder, { from: holder }); }); @@ -266,24 +272,24 @@ contract('ERC721Votes', function (accounts) { }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, - previousBalance: supply, + previousBalance: '1', newBalance: '0', }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holderDelegatee, previousBalance: '0', - newBalance: supply, + newBalance: '1', }); expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); - expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); + expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); }); }); @@ -293,8 +299,8 @@ contract('ERC721Votes', function (accounts) { }); it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -304,22 +310,22 @@ contract('ERC721Votes', function (accounts) { 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 { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); 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.holderVotes = '0'; 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' }); + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -333,15 +339,15 @@ contract('ERC721Votes', function (accounts) { 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) }); + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); 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.holderVotes = '0'; this.recipientVotes = '1'; }); @@ -368,54 +374,55 @@ contract('ERC721Votes', function (accounts) { describe('balanceOf', function () { it('grants to initial account', async function () { - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('4'); }); }); 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 + + await this.token.transfer(recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transfer(recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - - const t2 = await this.token.transfer(other2, 10, { from: recipient }); + + const t2 = await this.token.transfer(other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - - const t3 = await this.token.transfer(other2, 10, { from: recipient }); + + const t3 = await this.token.transfer(other2, NFT2, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - const t4 = await this.token.transfer(recipient, 20, { from: holder }); + const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '100' ]); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '90' ]); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '80' ]); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); + expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); + expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('1'); }); it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, '100', { from: holder }); + await this.token.transfer(recipient, NFT1, { from: holder }); + await this.token.transfer(recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const [ t1, t2, t3 ] = await batchInBlock([ () => this.token.delegate(other1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, NFT1, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, NFT2, { from: recipient, gas: 100000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); - // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - const t4 = await this.token.transfer(recipient, 20, { from: holder }); + const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); }); @@ -438,8 +445,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('4'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); }); it('returns zero if < first checkpoint block', async function () { @@ -449,39 +456,41 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder });//TODO: Make it NFT like - await time.advanceBlock(); + const total = await this.token.balanceOf(holder); + + const t1 = await this.token.delegate(other1, { from: holder }); await time.advanceBlock(); - const t2 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + const t2 = await this.token.transfer(other2, NFT1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.transfer(other2, 20, { from: holder }); + const t3 = await this.token.transfer(other2, NFT2, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 30, { from: other2 }); + const t4 = await this.token.transfer(holder, NFT2, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); - + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal(total); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(total); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); }); }); }); describe('getPastTotalSupply', function () { beforeEach(async function () { - await this.token.delegate(holder, { from: holder }); + // await this.token.delegate(holder, { from: holder }); }); it('reverts if block number >= current block', async function () { @@ -501,8 +510,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('returns zero if < first checkpoint block', async function () { @@ -512,7 +521,7 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { @@ -532,7 +541,6 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - console.log(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); From 4d3ca3a08346d886e4c7e33febd4dc072c5fbc45 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:23:31 -0400 Subject: [PATCH 097/300] Updating ERC721Vote tests descriptions --- test/token/ERC721/extensions/ERC721Votes.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index b2d56ea7ab0..49592f36fc8 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -99,7 +99,7 @@ contract('ERC721Votes', function (accounts) { describe('set delegation', function () { describe('call', function () { - it('delegation with balance', async function () {//TODO: Make it NFT like + it('delegation with tokenId', async function () { await this.token.mint(holder, supply); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); @@ -123,7 +123,7 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); }); - it('delegation without balance', async function () { + it('delegation without tokenId', async function () { expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); From d71fb499c4b9a25f500b2d51695a902e6a3218a0 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:55:45 -0400 Subject: [PATCH 098/300] Updating ERC721Vote contract and tests --- contracts/token/ERC721/extensions/ERC721Votes.sol | 2 -- test/token/ERC721/extensions/ERC721Votes.test.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 146ff1dd213..978bcd0787e 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -11,8 +11,6 @@ import "../../../utils/cryptography/ECDSA.sol"; * @dev Extension of ERC721 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 {ERC721VotesComp} 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 {getVotes} and {getPastVotes}. diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 49592f36fc8..9712e69d21e 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -490,7 +490,7 @@ contract('ERC721Votes', function (accounts) { describe('getPastTotalSupply', function () { beforeEach(async function () { - // await this.token.delegate(holder, { from: holder }); + await this.token.delegate(holder, { from: holder }); }); it('reverts if block number >= current block', async function () { From dd4a695ab74d85141490a36d948342512bb1b425 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 14:54:43 -0400 Subject: [PATCH 099/300] Finished tests --- test/token/ERC721/extensions/ERC721Votes.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 9712e69d21e..6d705c7c80f 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -415,16 +415,16 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const [ t1, t2, t3 ] = await batchInBlock([ - () => this.token.delegate(other1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, NFT1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, NFT2, { from: recipient, gas: 100000 }), + () => this.token.delegate(other1, { from: recipient, gas: 200000 }), + () => this.token.transfer(other2, NFT1, { from: recipient, gas: 200000 }), + () => this.token.transfer(other2, NFT2, { from: recipient, gas: 200000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); }); From 2d834df09679d976c3568db6955c30be1ae98e1f Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 16:24:20 -0400 Subject: [PATCH 100/300] Adding _afterTokenTransfer to base ERC271 contract --- .../extensions/GovernorVotesERC721.sol | 4 +- contracts/token/ERC721/ERC721.sol | 22 ++++ .../token/ERC721/extensions/ERC721Votes.sol | 19 +-- .../ERC721/extensions/ERC721Votes.test.js | 34 ++--- .../extensions/draft-ERC721Permit.test.js | 117 ------------------ 5 files changed, 44 insertions(+), 152 deletions(-) delete mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 9a2a4ac0ee8..383960d9ef1 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC72GovernorVotesERC721.sol) +// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC721GovernorVotesERC721.sol) pragma solidity ^0.8.0; @@ -12,7 +12,7 @@ import "../../utils/math/Math.sol"; * * _Available since v4.3._ */ -abstract contract ERC72GovernorVotesERC721 is Governor { +abstract contract ERC721GovernorVotesERC721 is Governor { ERC721Votes public immutable token; constructor(ERC721Votes tokenAddress) { diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 3c960417dcb..6fba12a2671 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -342,6 +342,8 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { _owners[tokenId] = to; emit Transfer(from, to, tokenId); + + _afterTokenTransfer(from, to, tokenId); } /** @@ -421,4 +423,24 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { address to, uint256 tokenId ) internal virtual {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `tokenId` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `tokenId` tokens have been minted for `to`. + * - when `to` is zero, `tokenId` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual {} } diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 978bcd0787e..18164dc6721 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; -import "./draft-ERC721Permit.sol"; +import "../ERC721.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; @@ -22,7 +22,7 @@ import "../../../utils/cryptography/ECDSA.sol"; * * _Available since v4.2._ */ -abstract contract ERC721Votes is ERC721Permit { +abstract contract ERC721Votes is ERC721 { struct Checkpoint { uint32 fromBlock; uint224 votes; @@ -191,7 +191,7 @@ abstract contract ERC721Votes is ERC721Permit { function _afterTokenTransfer( address from, address to - ) internal virtual { + ) internal virtual override{ _moveVotingPower(delegates(from), delegates(to), 1); } @@ -251,17 +251,4 @@ abstract contract ERC721Votes is ERC721Permit { function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } - - /** - * @dev Moves token from the caller's account to `recipient`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 tokenId) external returns (bool){ - _transfer(_msgSender(), recipient, tokenId); - _afterTokenTransfer(_msgSender(), recipient); - return true; - } } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 6d705c7c80f..1a114c1c6b2 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -299,7 +299,7 @@ contract('ERC721Votes', function (accounts) { }); it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); @@ -310,7 +310,7 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); @@ -324,7 +324,7 @@ contract('ERC721Votes', function (accounts) { it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -339,7 +339,7 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -381,20 +381,20 @@ contract('ERC721Votes', function (accounts) { describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { - await this.token.transfer(recipient, NFT1, { from: holder }); //give an account two tokens for readability - await this.token.transfer(recipient, NFT2, { from: holder }); + await this.token.transferFrom(recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transferFrom(recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - const t2 = await this.token.transfer(other2, NFT1, { from: recipient }); + const t2 = await this.token.transferFrom(other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - const t3 = await this.token.transfer(other2, NFT2, { from: recipient }); + const t3 = await this.token.transferFrom(other2, NFT2, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); @@ -410,19 +410,19 @@ contract('ERC721Votes', function (accounts) { }); it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, NFT1, { from: holder }); - await this.token.transfer(recipient, NFT2, { from: holder }); + await this.token.transferFrom(recipient, NFT1, { from: holder }); + await this.token.transferFrom(recipient, NFT2, { 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, NFT1, { from: recipient, gas: 200000 }), - () => this.token.transfer(other2, NFT2, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(other2, NFT1, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(other2, NFT2, { from: recipient, gas: 200000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); - const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); @@ -465,13 +465,13 @@ contract('ERC721Votes', function (accounts) { const t1 = await this.token.delegate(other1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t2 = await this.token.transfer(other2, NFT1, { from: holder }); + const t2 = await this.token.transferFrom(other2, NFT1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.transfer(other2, NFT2, { from: holder }); + const t3 = await this.token.transferFrom(other2, NFT2, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transfer(holder, NFT2, { from: other2 }); + const t4 = await this.token.transferFrom(holder, NFT2, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js deleted file mode 100644 index f22c904c724..00000000000 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable */ - -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); -const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; - -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; - -const ERC721PermitMock = artifacts.require('ERC721PermitMock'); - -const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); - -const Permit = [ - { name: 'owner', type: 'address' }, - { name: 'spender', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint256' }, -]; - -contract('ERC721Permit', function (accounts) { - const [ initialHolder, spender, recipient, other ] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - - const initialTokenId = new BN('100'); - - beforeEach(async function () { - this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); - - // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id - // from within the EVM as from the JSON RPC interface. - // See https://github.com/trufflesuite/ganache-core/issues/515 - this.chainId = await this.token.getChainId(); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect( - await this.token.DOMAIN_SEPARATOR(), - ).to.equal( - await domainSeparator(name, version, this.chainId, this.token.address), - ); - }); - - describe('permit', function () { - const wallet = Wallet.generate(); - - const owner = wallet.getAddressString(); - const value = initialTokenId; - const nonce = 0; - const maxDeadline = MAX_UINT256; - - const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ - primaryType: 'Permit', - types: { EIP712Domain, Permit }, - domain: { name, version, chainId, verifyingContract }, - message: { owner, spender, value, nonce, deadline }, - }); - - it('accepts owner signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); - expect(await this.token.getApproved(value)).to.be.equal(spender); - }); - - it('rejects reused signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects other signature', async function () { - const otherWallet = Wallet.generate(); - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects expired permit', async function () { - const deadline = (await time.latest()) - time.duration.weeks(1); - - const data = buildData(this.chainId, this.token.address, deadline); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, deadline, v, r, s), - 'ERC721Permit: expired deadline', - ); - }); - }); -}); From a207eb5318d6104e41f25ff7c59d7af4c7f15aac Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 16:30:31 -0400 Subject: [PATCH 101/300] Renaming supplied tokenId on tests --- .../ERC721/extensions/draft-ERC721Permit.sol | 87 ------------------- .../ERC721/extensions/draft-IERC721Permit.sol | 60 ------------- .../ERC721/extensions/ERC721Votes.test.js | 34 ++++---- 3 files changed, 17 insertions(+), 164 deletions(-) delete mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol delete mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol deleted file mode 100644 index c00d15367ab..00000000000 --- a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) - -pragma solidity ^0.8.0; - -import "./draft-IERC721Permit.sol"; -import "./ERC721Enumerable.sol"; -import "../../../utils/cryptography/draft-EIP712.sol"; -import "../../../utils/cryptography/ECDSA.sol"; -import "../../../utils/Counters.sol"; - -/** - * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - * - * _Available since v3.4._ - */ -abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { - using Counters for Counters.Counter; - - mapping(address => Counters.Counter) private _nonces; - - // solhint-disable-next-line var-name-mixedcase - bytes32 private immutable _PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - - /** - * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. - * - * It's a good idea to use the same `name` that is defined as the ERC721 token name. - */ - constructor(string memory name) EIP712(name, "1") {} - - /** - * @dev See {IERC721Permit-permit}. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual override { - require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); - - bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); - - bytes32 hash = _hashTypedDataV4(structHash); - - address signer = ECDSA.recover(hash, v, r, s); - require(signer == owner, "ERC721Permit: invalid signature"); - - _approve(spender, value); - } - - /** - * @dev See {IERC721Permit-nonces}. - */ - function nonces(address owner) public view virtual override returns (uint256) { - return _nonces[owner].current(); - } - - /** - * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view override returns (bytes32) { - return _domainSeparatorV4(); - } - - /** - * @dev "Consume a nonce": return the current value and increment. - * - * _Available since v4.1._ - */ - function _useNonce(address owner) internal virtual returns (uint256 current) { - Counters.Counter storage nonce = _nonces[owner]; - current = nonce.current(); - nonce.increment(); - } -} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol deleted file mode 100644 index 61882f2de0d..00000000000 --- a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - */ -interface IERC721Permit { - /** - * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, - * given ``owner``'s signed approval. - * - * IMPORTANT: The same issues {IERC721-approve} has related to transaction - * ordering also apply here. - * - * Emits an {Approval} event. - * - * Requirements: - * - * - `spender` cannot be the zero address. - * - `deadline` must be a timestamp in the future. - * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` - * over the EIP712-formatted function arguments. - * - the signature must use ``owner``'s current nonce (see {nonces}). - * - * For more information on the signature format, see the - * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP - * section]. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - /** - * @dev Returns the current nonce for `owner`. This value must be - * included whenever a signature is generated for {permit}. - * - * Every successful call to {permit} increases ``owner``'s nonce by one. This - * prevents a signature from being used multiple times. - */ - function nonces(address owner) external view returns (uint256); - - /** - * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view returns (bytes32); -} diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 1a114c1c6b2..21d5587d66f 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -61,7 +61,7 @@ contract('ERC721Votes', function (accounts) { const name = 'My Token'; const symbol = 'MTKN'; const version = '1'; - const supply = new BN('10000000000000000000000000'); + const initalTokenId = new BN('10000000000000000000000000'); beforeEach(async function () { this.token = await ERC721VotesMock.new(name, symbol); @@ -89,7 +89,7 @@ contract('ERC721Votes', function (accounts) { this.token.mint(holder, NFT1); this.token.mint(holder, NFT2); this.token.mint(holder, NFT3); - this.token.mint(holder, supply); + this.token.mint(holder, initalTokenId); await expectRevert( this.token.mint(holder, lastTokenId), @@ -100,7 +100,7 @@ contract('ERC721Votes', function (accounts) { describe('set delegation', function () { describe('call', function () { it('delegation with tokenId', async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, initalTokenId); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -151,7 +151,7 @@ contract('ERC721Votes', function (accounts) { }}); beforeEach(async function () { - await this.token.mint(delegatorAddress, supply); + await this.token.mint(delegatorAddress, initalTokenId); }); it('accept signed delegation', async function () { @@ -257,7 +257,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, initalTokenId); await this.token.delegate(holder, { from: holder }); }); @@ -295,12 +295,12 @@ contract('ERC721Votes', function (accounts) { describe('transfers', function () { beforeEach(async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, initalTokenId); }); it('no delegation', async function () { - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -310,8 +310,8 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -324,8 +324,8 @@ contract('ERC721Votes', function (accounts) { it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -339,8 +339,8 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -366,7 +366,7 @@ contract('ERC721Votes', function (accounts) { // 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); + await this.token.mint(holder, initalTokenId); await this.token.mint(holder, NFT1); await this.token.mint(holder, NFT2); await this.token.mint(holder, NFT3); @@ -505,7 +505,7 @@ contract('ERC721Votes', function (accounts) { }); it('returns the latest block if >= last checkpoint block', async function () { - t1 = await this.token.mint(holder, supply); + t1 = await this.token.mint(holder, initalTokenId); await time.advanceBlock(); await time.advanceBlock(); @@ -516,7 +516,7 @@ contract('ERC721Votes', function (accounts) { it('returns zero if < first checkpoint block', async function () { await time.advanceBlock(); - const t1 = await this.token.mint(holder, supply); + const t1 = await this.token.mint(holder, initalTokenId); await time.advanceBlock(); await time.advanceBlock(); From 052d4a12a86968bebab8752435273ecf09be7a62 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 17:45:23 -0400 Subject: [PATCH 102/300] Updating tests based on new contract changes --- contracts/mocks/ERC721PermitMock.sol | 20 -------- contracts/mocks/ERC721VotesMock.sol | 2 +- .../token/ERC721/extensions/ERC721Votes.sol | 50 +++++++++++++++++-- .../ERC721/extensions/ERC721Votes.test.js | 34 ++++++------- 4 files changed, 63 insertions(+), 43 deletions(-) delete mode 100644 contracts/mocks/ERC721PermitMock.sol diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol deleted file mode 100644 index 37a860ef4dc..00000000000 --- a/contracts/mocks/ERC721PermitMock.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../token/ERC721/extensions/draft-ERC721Permit.sol"; - -contract ERC721PermitMock is ERC721Permit { - constructor( - string memory name, - string memory symbol, - address initialAccount, - uint256 tokenId - ) payable ERC721(name, symbol) ERC721Permit(name) { - _mint(initialAccount, tokenId); - } - - function getChainId() external view returns (uint256) { - return block.chainid; - } -} diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 09e40a4ea85..3b7a1bed7a8 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { - constructor(string memory name, string memory symbol) ERC721(name, symbol) ERC721Permit(name) {} + constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {} function mint(address account, uint256 tokenId) public { _mint(account, tokenId); diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 18164dc6721..00a5e8707c0 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -4,9 +4,11 @@ pragma solidity ^0.8.0; import "../ERC721.sol"; +import "../../../utils/Counters.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; /** * @dev Extension of ERC721 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. @@ -22,19 +24,29 @@ import "../../../utils/cryptography/ECDSA.sol"; * * _Available since v4.2._ */ -abstract contract ERC721Votes is ERC721 { +abstract contract ERC721Votes is ERC721, EIP712 { + using Counters for Counters.Counter; + struct Checkpoint { uint32 fromBlock; uint224 votes; } - + uint256 _totalSupply; bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); mapping(address => address) private _delegates; + mapping(address => Counters.Counter) private _nonces; mapping(address => Checkpoint[]) private _checkpoints; Checkpoint[] private _totalSupplyCheckpoints; + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC721 token name. + + constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ + /** * @dev Emitted when an account changes their delegate. */ @@ -169,7 +181,8 @@ abstract contract ERC721Votes is ERC721 { */ function _mint(address account, uint256 tokenId) internal virtual override { super._mint(account, tokenId); - require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + _totalSupply += 1; + require(_totalSupply <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } @@ -179,7 +192,7 @@ abstract contract ERC721Votes is ERC721 { */ function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); - + _totalSupply -= 1; _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); } @@ -190,7 +203,8 @@ abstract contract ERC721Votes is ERC721 { */ function _afterTokenTransfer( address from, - address to + address to, + uint256 tokenId ) internal virtual override{ _moveVotingPower(delegates(from), delegates(to), 1); } @@ -244,6 +258,32 @@ abstract contract ERC721Votes is ERC721 { } } + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } + + /** + * @dev Returns an address nonce. + */ + function nonces(address owner) public view virtual returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev Returns DOMAIN_SEPARATOR. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparatorV4(); + } + function _add(uint256 a, uint256 b) private pure returns (uint256) { return a + b; } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 21d5587d66f..a5b9cd16198 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -299,7 +299,7 @@ contract('ERC721Votes', function (accounts) { }); it('no delegation', async function () { - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); @@ -310,7 +310,7 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); @@ -324,7 +324,7 @@ contract('ERC721Votes', function (accounts) { it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -339,7 +339,7 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -381,20 +381,20 @@ contract('ERC721Votes', function (accounts) { describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { - await this.token.transferFrom(recipient, NFT1, { from: holder }); //give an account two tokens for readability - await this.token.transferFrom(recipient, NFT2, { from: holder }); + await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - const t2 = await this.token.transferFrom(other2, NFT1, { from: recipient }); + const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - const t3 = await this.token.transferFrom(other2, NFT2, { from: recipient }); + const t3 = await this.token.transferFrom(recipient, other2, NFT2, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); @@ -410,19 +410,19 @@ contract('ERC721Votes', function (accounts) { }); it('does not add more than one checkpoint in a block', async function () { - await this.token.transferFrom(recipient, NFT1, { from: holder }); - await this.token.transferFrom(recipient, NFT2, { from: holder }); + await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); + await this.token.transferFrom(holder, recipient, NFT2, { 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.transferFrom(other2, NFT1, { from: recipient, gas: 200000 }), - () => this.token.transferFrom(other2, NFT2, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(recipient, other2, NFT1, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(recipient, other2, NFT2, { from: recipient, gas: 200000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); - const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); @@ -465,13 +465,13 @@ contract('ERC721Votes', function (accounts) { const t1 = await this.token.delegate(other1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t2 = await this.token.transferFrom(other2, NFT1, { from: holder }); + const t2 = await this.token.transferFrom(holder, other2, NFT1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.transferFrom(other2, NFT2, { from: holder }); + const t3 = await this.token.transferFrom(holder, other2, NFT2, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transferFrom(holder, NFT2, { from: other2 }); + const t4 = await this.token.transferFrom(other2, holder, NFT2, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); From adbbbfb1ac94bbe4d6abf584dc9092455187d605 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 20:15:54 -0400 Subject: [PATCH 103/300] Updating execution order inside of mint --- contracts/token/ERC721/extensions/ERC721Votes.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 00a5e8707c0..7871282643b 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -180,10 +180,11 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been increased. */ function _mint(address account, uint256 tokenId) internal virtual override { + require(_totalSupply+1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + super._mint(account, tokenId); _totalSupply += 1; - require(_totalSupply <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } From 4899be8c63cf2473e5d4ba1b76a202ce3165ba46 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 16:25:14 -0400 Subject: [PATCH 104/300] Adding Mocks for testing and integrating nft minting to current workflow test --- .../extensions/GovernorVotesERC721.sol | 2 +- contracts/mocks/GovernorERC721Mock.sol | 41 +++++++++ test/governance/GovernorWorkflow.behavior.js | 4 +- .../extensions/GovernorERC721.test.js | 91 +++++++++++++++++++ 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 contracts/mocks/GovernorERC721Mock.sol create mode 100644 test/governance/extensions/GovernorERC721.test.js diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 383960d9ef1..2e3079fc243 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -12,7 +12,7 @@ import "../../utils/math/Math.sol"; * * _Available since v4.3._ */ -abstract contract ERC721GovernorVotesERC721 is Governor { +abstract contract GovernorVotesERC721 is Governor { ERC721Votes public immutable token; constructor(ERC721Votes tokenAddress) { diff --git a/contracts/mocks/GovernorERC721Mock.sol b/contracts/mocks/GovernorERC721Mock.sol new file mode 100644 index 00000000000..7508f168334 --- /dev/null +++ b/contracts/mocks/GovernorERC721Mock.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../governance/extensions/GovernorCountingSimple.sol"; +import "../governance/extensions/GovernorVotesERC721.sol"; + +contract GovernorERC721Mock is GovernorVotesERC721, GovernorCountingSimple { + constructor(string memory name_, ERC721Votes token_) Governor(name_) GovernorVotesERC721(token_) {} + + 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; + } + + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 salt + ) public returns (uint256 proposalId) { + return _cancel(targets, values, calldatas, salt); + } + + function getVotes(address account, uint256 blockNumber) + public + view + virtual + override(IGovernor, GovernorVotesERC721) + returns (uint256) + { + return super.getVotes(account, blockNumber); + } +} diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js index 70319cd44d3..e8e2416b19e 100644 --- a/test/governance/GovernorWorkflow.behavior.js +++ b/test/governance/GovernorWorkflow.behavior.js @@ -31,7 +31,9 @@ function runGovernorWorkflow () { for (const voter of this.settings.voters) { if (voter.weight) { await this.token.transfer(voter.voter, voter.weight, { from: this.settings.tokenHolder }); - } + }else if(voter.nftWeight){ + await this.token.transferFrom(this.settings.tokenHolder, voter.voter, voter.nftWeight, { from: this.settings.tokenHolder }); + } } } diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js new file mode 100644 index 00000000000..845d5c20d21 --- /dev/null +++ b/test/governance/extensions/GovernorERC721.test.js @@ -0,0 +1,91 @@ +const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const Enums = require('../../helpers/enums'); + +const { + runGovernorWorkflow, +} = require('./../GovernorWorkflow.behavior'); + +const Token = artifacts.require('ERC721VotesMock'); +const Governor = artifacts.require('GovernorERC721Mock'); +const CallReceiver = artifacts.require('CallReceiverMock'); + +contract('GovernorERC721Mock', function (accounts) { + const [ owner, voter1, voter2, voter3, voter4 ] = accounts; + + const name = 'OZ-Governor'; + const tokenName = 'MockNFToken'; + const tokenSymbol = 'MTKN'; + const initalTokenId = web3.utils.toWei('100'); + const NFT1 = web3.utils.toWei('10'); + const NFT2 = web3.utils.toWei('20'); + const NFT3 = web3.utils.toWei('30'); + + beforeEach(async function () { + this.owner = owner; + this.token = await Token.new(tokenName, tokenSymbol); + this.mock = await Governor.new(name, this.token.address); + this.receiver = await CallReceiver.new(); + await this.token.mint(owner, initalTokenId); + await this.token.mint(owner, NFT1); + await this.token.mint(owner, NFT2); + await this.token.mint(owner, NFT3); + + await this.token.delegate(voter1, { from: voter1 }); + await this.token.delegate(voter2, { from: voter2 }); + await this.token.delegate(voter3, { from: voter3 }); + await this.token.delegate(voter4, { from: voter4 }); + }); + + 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('4'); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16'); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + }); + + describe.only('voting with ERC721 token', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, nftWeight: initalTokenId, support: Enums.VoteType.For }, + { voter: voter2, nftWeight: NFT1, support: Enums.VoteType.For }, + { voter: voter3, nftWeight: NFT2, support: Enums.VoteType.Against }, + { voter: voter4, nftWeight: NFT3, support: Enums.VoteType.Abstain }, + ] + } + }); + + afterEach(async function () { + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter3)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter4)).to.be.equal(true); + + this.receipts.castVote.filter(Boolean).forEach(vote => { + const { voter } = vote.logs.find(Boolean).args; + expectEvent( + vote, + 'VoteCast', + this.settings.voters.find(({ address }) => address === voter), + ); + }); + await this.mock.proposalVotes(this.id).then(result => { + for (const [key, value] of Object.entries(Enums.VoteType)) { + expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( + Object.values(this.settings.voters).filter(({ support }) => support === value).length.toString() + ); + } + }); + }); + runGovernorWorkflow(); + }); +}); From 1f850571a6b3bc2410bdc7b50be67382c84b9447 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 17:02:56 -0400 Subject: [PATCH 105/300] Implementing override test --- .../extensions/GovernorERC721.test.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 845d5c20d21..4f81055800a 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -64,20 +64,22 @@ contract('GovernorERC721Mock', function (accounts) { }); afterEach(async function () { - expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(true); - expect(await this.mock.hasVoted(this.id, voter3)).to.be.equal(true); - expect(await this.mock.hasVoted(this.id, voter4)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); - this.receipts.castVote.filter(Boolean).forEach(vote => { + for(const vote of this.receipts.castVote.filter(Boolean)){ const { voter } = vote.logs.find(Boolean).args; + + expect(await this.mock.hasVoted(this.id, voter)).to.be.equal(true); + expectEvent( vote, 'VoteCast', this.settings.voters.find(({ address }) => address === voter), ); - }); + + expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); + } + await this.mock.proposalVotes(this.id).then(result => { for (const [key, value] of Object.entries(Enums.VoteType)) { expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( @@ -86,6 +88,8 @@ contract('GovernorERC721Mock', function (accounts) { } }); }); + runGovernorWorkflow(); + }); }); From 3d0f1e40006d52b189a311e5d47c4cdfb47212da Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 17:47:26 -0400 Subject: [PATCH 106/300] Removing .only from tests --- test/governance/extensions/GovernorERC721.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 4f81055800a..ee8ad8399da 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -44,7 +44,7 @@ contract('GovernorERC721Mock', function (accounts) { expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); }); - describe.only('voting with ERC721 token', function () { + describe('voting with ERC721 token', function () { beforeEach(async function () { this.settings = { proposal: [ From 9a3f2a17e055398d84b23d6496339d6fc390bd67 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 18:05:24 -0400 Subject: [PATCH 107/300] Updating contracts READMEs --- contracts/governance/README.adoc | 4 ++++ contracts/token/ERC721/README.adoc | 2 ++ 2 files changed, 6 insertions(+) diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index d388f4e3a42..ef5d416c012 100644 --- a/contracts/governance/README.adoc +++ b/contracts/governance/README.adoc @@ -22,6 +22,8 @@ Votes modules determine the source of voting power, and sometimes quorum number. * {GovernorVotes}: Extracts voting weight from an {ERC20Votes} token. +* {GovernorVotesERC721}: Extracts voting weight from 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. @@ -64,6 +66,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you {{GovernorVotesQuorumFraction}} +{{GovernorVotesERC721}} + {{GovernorVotesComp}} === Extensions diff --git a/contracts/token/ERC721/README.adoc b/contracts/token/ERC721/README.adoc index f1122c53a99..51089e1627c 100644 --- a/contracts/token/ERC721/README.adoc +++ b/contracts/token/ERC721/README.adoc @@ -41,6 +41,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel {{ERC721URIStorage}} +{{ERC721Votes}} + == Presets These contracts are preconfigured combinations of the above features. They can be used through inheritance or as models to copy and paste their source code. From f3d8453278766aecd50a91022fbf960b5d9d3e9d Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 18:57:08 -0400 Subject: [PATCH 108/300] Governance adocs update --- contracts/mocks/ERC721VotesMock.sol | 2 +- contracts/mocks/UserTOkenerc721Mock.sol | 32 +++++++++++++ .../token/ERC721/extensions/ERC721Votes.sol | 3 +- docs/modules/ROOT/pages/erc721.adoc | 2 +- docs/modules/ROOT/pages/governance.adoc | 45 ++++++++++++++++++- 5 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 3b7a1bed7a8..bde65e5a5ff 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { - constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {} + constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} function mint(address account, uint256 tokenId) public { _mint(account, tokenId); diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol new file mode 100644 index 00000000000..b3874e66da5 --- /dev/null +++ b/contracts/mocks/UserTOkenerc721Mock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; + +contract MyToken is ERC721, EIP712, ERC721Votes { + constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._burn(account, amount); + } +} \ No newline at end of file diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 7871282643b..39c03934028 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -44,8 +44,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. * * It's a good idea to use the same `name` that is defined as the ERC721 token name. - - constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ + */ /** * @dev Emitted when an account changes their delegate. diff --git a/docs/modules/ROOT/pages/erc721.adoc b/docs/modules/ROOT/pages/erc721.adoc index 8d28fad2e6e..14dbdc97606 100644 --- a/docs/modules/ROOT/pages/erc721.adoc +++ b/docs/modules/ROOT/pages/erc721.adoc @@ -1,6 +1,6 @@ = ERC721 -We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate* or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. +We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate*, *voting rights*, or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. ERC721 is a more complex standard than ERC20, with multiple optional extensions, and is split across a number of contracts. The OpenZeppelin Contracts provide flexibility regarding how these are combined, along with custom useful extensions. Check out the xref:api:token/ERC721.adoc[API Reference] to learn more about these. diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 027301c1ba5..a262a75af38 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -14,6 +14,10 @@ This governance protocol is generally implemented in a special-purpose contract OpenZeppelin’s Governor system was designed with a concern for compatibility with existing systems that were based on Compound’s GovernorAlpha and GovernorBravo. Because of this, you will find that many modules are presented in two variants, one of which is built for compatibility with those systems. +=== ERC721Votes + +The ERC721 extension to keep track of votes and vote delegation is one such case. + === ERC20Votes & ERC20VotesComp The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. @@ -119,11 +123,48 @@ contract MyToken is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper { } ``` +If your project requires The voting power of each account in our governance setup will be determined by an ERC721 token. The token has to implement the ERC721Votes extension. This extension will keep track of historical balances so that voting power is retrieved from past snapshots rather than current balance, which is an important protection that prevents double voting. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; + +contract MyToken is ERC721, EIP712, ERC721Votes { + constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._burn(account, amount); + } +} +``` + NOTE: Voting power could be determined in different ways: multiple ERC20 tokens, ERC721 tokens, sybil resistant identities, etc. All of these options are potentially supported by writing a custom Votes module for your Governor. === Governor -Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, and 3) what options people have when casting a vote and how those votes are counted. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. +Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, 3) what options people have when casting a vote and how those votes are counted, and 4)what type of token should be use to vote. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. For 1) we will use the GovernorVotes module, which hooks to an ERC20Votes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. @@ -131,6 +172,8 @@ For 2) we will use GovernorVotesQuorumFraction which works together with ERC20Vo For 3) we will use GovernorCountingSimple, a module that offers 3 options to voters: For, Against, and Abstain, and where only For and Abstain votes are counted towards quorum. +For 4) we will use the GovernorVotesERC721 module, which hooks to an ERC721Votes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. + Besides these modules, Governor itself has some parameters we must set. votingDelay: How long after a proposal is created should voting power be fixed. A large voting delay gives users time to unstake tokens if necessary. From ec047acd530cd821e44e1f73c32c8a0ff369769b Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 14:13:44 -0400 Subject: [PATCH 109/300] Removing test contract --- contracts/mocks/UserTOkenerc721Mock.sol | 32 ------------------------- 1 file changed, 32 deletions(-) delete mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol deleted file mode 100644 index b3874e66da5..00000000000 --- a/contracts/mocks/UserTOkenerc721Mock.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "@openzeppelin/contracts/token/ERC721.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; - -contract MyToken is ERC721, EIP712, ERC721Votes { - constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._afterTokenTransfer(from, to, amount); - } - - function _mint(address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._mint(to, amount); - } - - function _burn(address account, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._burn(account, amount); - } -} \ No newline at end of file From 1179e909bc2bee61d6374ce24cc29a9cf85712a8 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sat, 30 Oct 2021 20:49:29 -0400 Subject: [PATCH 110/300] Initial contracts creation --- .../extensions/GovernorVotesERC721.sol | 2 +- .../ERC721/extensions/draft-ERC721Permit.sol | 87 +++++++++++++++++++ .../ERC721/extensions/draft-IERC721Permit.sol | 60 +++++++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol create mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 2e3079fc243..c2472a2e4e2 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC721GovernorVotesERC721.sol) +// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC72GovernorVotesERC721.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol new file mode 100644 index 00000000000..c00d15367ab --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) + +pragma solidity ^0.8.0; + +import "./draft-IERC721Permit.sol"; +import "./ERC721Enumerable.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; +import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/Counters.sol"; + +/** + * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * _Available since v3.4._ + */ +abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { + using Counters for Counters.Counter; + + mapping(address => Counters.Counter) private _nonces; + + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC721 token name. + */ + constructor(string memory name) EIP712(name, "1") {} + + /** + * @dev See {IERC721Permit-permit}. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); + + bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + + bytes32 hash = _hashTypedDataV4(structHash); + + address signer = ECDSA.recover(hash, v, r, s); + require(signer == owner, "ERC721Permit: invalid signature"); + + _approve(spender, value); + } + + /** + * @dev See {IERC721Permit-nonces}. + */ + function nonces(address owner) public view virtual override returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } +} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol new file mode 100644 index 00000000000..61882f2de0d --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + */ +interface IERC721Permit { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC721-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} From 913946bcc09d6463cd5cc6e79b40fe64cf5c2eee Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 09:28:35 -0400 Subject: [PATCH 111/300] creating permit tests --- contracts/mocks/ERC721PermitMock.sol | 20 ++ .../ERC721/extensions/ERC721Votes.test.js | 233 +++++++++++++++++- .../extensions/draft-ERC721Permit.test.js | 117 +++++++++ 3 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 contracts/mocks/ERC721PermitMock.sol create mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol new file mode 100644 index 00000000000..37a860ef4dc --- /dev/null +++ b/contracts/mocks/ERC721PermitMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC721/extensions/draft-ERC721Permit.sol"; + +contract ERC721PermitMock is ERC721Permit { + constructor( + string memory name, + string memory symbol, + address initialAccount, + uint256 tokenId + ) payable ERC721(name, symbol) ERC721Permit(name) { + _mint(initialAccount, tokenId); + } + + function getChainId() external view returns (uint256) { + return block.chainid; + } +} diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index a5b9cd16198..3d925cc89b5 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -14,7 +14,6 @@ const queue = promisify(setImmediate); const ERC721VotesMock = artifacts.require('ERC721VotesMock'); const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); -const { Console } = require('console'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -55,6 +54,7 @@ async function batchInBlock (txs) { contract('ERC721Votes', function (accounts) { const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; +<<<<<<< HEAD const NFT1 = new BN('10'); const NFT2 = new BN('20'); const NFT3 = new BN('30'); @@ -62,6 +62,13 @@ contract('ERC721Votes', function (accounts) { const symbol = 'MTKN'; const version = '1'; const initalTokenId = new BN('10000000000000000000000000'); +======= + + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; + const supply = new BN('10000000000000000000000000'); +>>>>>>> creating permit tests beforeEach(async function () { this.token = await ERC721VotesMock.new(name, symbol); @@ -85,6 +92,7 @@ contract('ERC721Votes', function (accounts) { }); it('minting restriction', async function () { +<<<<<<< HEAD const lastTokenId = new BN('2').pow(new BN('224')); this.token.mint(holder, NFT1); this.token.mint(holder, NFT2); @@ -93,14 +101,24 @@ contract('ERC721Votes', function (accounts) { await expectRevert( this.token.mint(holder, lastTokenId), +======= + const amount = new BN('2').pow(new BN('224')); + await expectRevert( + this.token.mint(holder, amount), +>>>>>>> creating permit tests 'ERC721Votes: total supply risks overflowing votes', ); }); describe('set delegation', function () { describe('call', function () { +<<<<<<< HEAD it('delegation with tokenId', async function () { await this.token.mint(holder, initalTokenId); +======= + it('delegation with balance', async function () { + await this.token.mint(holder, supply); +>>>>>>> creating permit tests expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -112,11 +130,16 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '0', +<<<<<<< HEAD newBalance: '1', +======= + newBalance: supply, +>>>>>>> creating permit tests }); expect(await this.token.delegates(holder)).to.be.equal(holder); +<<<<<<< HEAD expect(await this.token.getVotes(holder)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); @@ -124,6 +147,15 @@ contract('ERC721Votes', function (accounts) { }); it('delegation without tokenId', async function () { +======= + expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); + }); + + it('delegation without balance', async function () { +>>>>>>> creating permit tests expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -151,7 +183,11 @@ contract('ERC721Votes', function (accounts) { }}); beforeEach(async function () { +<<<<<<< HEAD await this.token.mint(delegatorAddress, initalTokenId); +======= + await this.token.mint(delegatorAddress, supply); +>>>>>>> creating permit tests }); it('accept signed delegation', async function () { @@ -175,15 +211,26 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: delegatorAddress, previousBalance: '0', +<<<<<<< HEAD newBalance: '1', +======= + newBalance: supply, +>>>>>>> creating permit tests }); expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); +<<<<<<< HEAD expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1'); +======= + expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); +>>>>>>> creating permit tests }); it('rejects reused signature', async function () { @@ -257,7 +304,11 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { +<<<<<<< HEAD await this.token.mint(holder, initalTokenId); +======= + await this.token.mint(holder, supply); +>>>>>>> creating permit tests await this.token.delegate(holder, { from: holder }); }); @@ -272,35 +323,61 @@ contract('ERC721Votes', function (accounts) { }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, +<<<<<<< HEAD previousBalance: '1', +======= + previousBalance: supply, +>>>>>>> creating permit tests newBalance: '0', }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holderDelegatee, previousBalance: '0', +<<<<<<< HEAD newBalance: '1', +======= + newBalance: supply, +>>>>>>> creating permit tests }); expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); +<<<<<<< HEAD expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); +======= + expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); +>>>>>>> creating permit tests }); }); describe('transfers', function () { beforeEach(async function () { +<<<<<<< HEAD await this.token.mint(holder, initalTokenId); }); it('no delegation', async function () { const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); +======= + 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' }); +>>>>>>> creating permit tests expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -310,22 +387,37 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); +<<<<<<< HEAD const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); +======= + 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) }); +>>>>>>> creating permit tests const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); expect(receipt.logs.filter(({ event }) => event == 'DelegateVotesChanged').every(({ logIndex }) => transferLogIndex < logIndex)).to.be.equal(true); +<<<<<<< HEAD this.holderVotes = '0'; +======= + this.holderVotes = supply.subn(1); +>>>>>>> creating permit tests this.recipientVotes = '0'; }); it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); +<<<<<<< HEAD const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); +======= + const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); +>>>>>>> creating permit tests expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -339,15 +431,25 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); +<<<<<<< HEAD const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); +======= + 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) }); +>>>>>>> creating permit tests 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); +<<<<<<< HEAD this.holderVotes = '0'; +======= + this.holderVotes = supply.subn(1); +>>>>>>> creating permit tests this.recipientVotes = '1'; }); @@ -366,27 +468,40 @@ contract('ERC721Votes', function (accounts) { // 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 () { +<<<<<<< HEAD await this.token.mint(holder, initalTokenId); await this.token.mint(holder, NFT1); await this.token.mint(holder, NFT2); await this.token.mint(holder, NFT3); +======= + await this.token.mint(holder, supply); +>>>>>>> creating permit tests }); describe('balanceOf', function () { it('grants to initial account', async function () { +<<<<<<< HEAD expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('4'); +======= + expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); +>>>>>>> creating permit tests }); }); describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { +<<<<<<< HEAD await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); +======= + await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability +>>>>>>> creating permit tests expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); +<<<<<<< HEAD const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); @@ -425,6 +540,47 @@ contract('ERC721Votes', function (accounts) { const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); +======= + + const t2 = await this.token.transfer(other2, 10, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + + const t3 = await this.token.transfer(other2, 10, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); + + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '100' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '90' ]); + expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '80' ]); + expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + + await time.advanceBlock(); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); + }); + + it('does not add more than one checkpoint in a block', async function () { + await this.token.transfer(recipient, '100', { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const [ t1, t2, t3 ] = await batchInBlock([ + () => this.token.delegate(other1, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + ]); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); + // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check + // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); +>>>>>>> creating permit tests }); }); @@ -445,8 +601,13 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); +<<<<<<< HEAD expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('4'); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); +======= + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); +>>>>>>> creating permit tests }); it('returns zero if < first checkpoint block', async function () { @@ -456,6 +617,7 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); +<<<<<<< HEAD expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); }); @@ -484,6 +646,34 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3'); expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); +======= + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.transfer(holder, 20, { from: other2 }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); +>>>>>>> creating permit tests }); }); }); @@ -505,22 +695,36 @@ contract('ERC721Votes', function (accounts) { }); it('returns the latest block if >= last checkpoint block', async function () { +<<<<<<< HEAD t1 = await this.token.mint(holder, initalTokenId); +======= + t1 = await this.token.mint(holder, supply); +>>>>>>> creating permit tests await time.advanceBlock(); await time.advanceBlock(); +<<<<<<< HEAD expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); +======= + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); +>>>>>>> creating permit tests }); it('returns zero if < first checkpoint block', async function () { await time.advanceBlock(); +<<<<<<< HEAD const t1 = await this.token.mint(holder, initalTokenId); +======= + const t1 = await this.token.mint(holder, supply); +>>>>>>> creating permit tests await time.advanceBlock(); await time.advanceBlock(); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); +<<<<<<< HEAD expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); @@ -538,10 +742,27 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); const t5 = await this.token.mint(holder, NFT3); +======= + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.mint(holder, supply); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.burn(10); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.burn(10); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.mint(holder, 20); +>>>>>>> creating permit tests await time.advanceBlock(); await time.advanceBlock(); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); +<<<<<<< HEAD expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); @@ -552,6 +773,16 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); +======= + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); +>>>>>>> creating permit tests }); }); }); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js new file mode 100644 index 00000000000..7a7b8cf5d83 --- /dev/null +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -0,0 +1,117 @@ +/* eslint-disable */ + +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const ERC721PermitMock = artifacts.require('ERC721PermitMock'); + +const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); + +const Permit = [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, +]; + +contract('ERC721Permit', function (accounts) { + const [ initialHolder, spender, recipient, other ] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; + + const initialTokenId = new BN('100'); + + beforeEach(async function () { + this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); + + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = await this.token.getChainId(); + }); + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect( + await this.token.DOMAIN_SEPARATOR(), + ).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address), + ); + }); + + describe.only('permit', function () { + const wallet = Wallet.generate(); + + const owner = wallet.getAddressString(); + const value = initialTokenId; + const nonce = 0; + const maxDeadline = MAX_UINT256; + + const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ + primaryType: 'Permit', + types: { EIP712Domain, Permit }, + domain: { name, version, chainId, verifyingContract }, + message: { owner, spender, value, nonce, deadline }, + }); + + it('accepts owner signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); + expect(await this.token.getApproved(value)).to.be.equal(spender); + }); + + it('rejects reused signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects other signature', async function () { + const otherWallet = Wallet.generate(); + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects expired permit', async function () { + const deadline = (await time.latest()) - time.duration.weeks(1); + + const data = buildData(this.chainId, this.token.address, deadline); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, deadline, v, r, s), + 'ERC721Permit: expired deadline', + ); + }); + }); +}); From e999d78355547596e9f9831ebf2d197047200504 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 10:26:22 -0400 Subject: [PATCH 112/300] Fixing checkpoints count --- .../token/ERC721/extensions/ERC721Votes.sol | 8 +- .../ERC721/extensions/ERC721Votes.test.js | 312 +++--------------- .../extensions/draft-ERC721Permit.test.js | 2 +- 3 files changed, 44 insertions(+), 278 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 39c03934028..6a7ef27dbec 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -8,7 +8,6 @@ import "../../../utils/Counters.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; -import "../../../utils/cryptography/draft-EIP712.sol"; /** * @dev Extension of ERC721 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. @@ -179,11 +178,9 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been increased. */ function _mint(address account, uint256 tokenId) internal virtual override { - require(_totalSupply+1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - super._mint(account, tokenId); - _totalSupply += 1; - + require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } @@ -192,7 +189,6 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); - _totalSupply -= 1; _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 3d925cc89b5..81397dda8aa 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -53,22 +53,15 @@ async function batchInBlock (txs) { } contract('ERC721Votes', function (accounts) { - const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; -<<<<<<< HEAD + const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; + const NFT0 = new BN('10000000000000000000000000'); const NFT1 = new BN('10'); const NFT2 = new BN('20'); - const NFT3 = new BN('30'); + const NFT3 = new BN('30'); + const NFT4 = new BN('40'); const name = 'My Token'; const symbol = 'MTKN'; const version = '1'; - const initalTokenId = new BN('10000000000000000000000000'); -======= - - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - const supply = new BN('10000000000000000000000000'); ->>>>>>> creating permit tests beforeEach(async function () { this.token = await ERC721VotesMock.new(name, symbol); @@ -92,33 +85,23 @@ contract('ERC721Votes', function (accounts) { }); it('minting restriction', async function () { -<<<<<<< HEAD const lastTokenId = new BN('2').pow(new BN('224')); this.token.mint(holder, NFT1); this.token.mint(holder, NFT2); this.token.mint(holder, NFT3); - this.token.mint(holder, initalTokenId); + this.token.mint(holder, NFT0); + this.token.mint(holder, NFT4); await expectRevert( this.token.mint(holder, lastTokenId), -======= - const amount = new BN('2').pow(new BN('224')); - await expectRevert( - this.token.mint(holder, amount), ->>>>>>> creating permit tests 'ERC721Votes: total supply risks overflowing votes', ); }); describe('set delegation', function () { describe('call', function () { -<<<<<<< HEAD - it('delegation with tokenId', async function () { - await this.token.mint(holder, initalTokenId); -======= - it('delegation with balance', async function () { - await this.token.mint(holder, supply); ->>>>>>> creating permit tests + it('delegation with tokens', async function () { + await this.token.mint(holder, NFT0); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -130,32 +113,18 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '0', -<<<<<<< HEAD newBalance: '1', -======= - newBalance: supply, ->>>>>>> creating permit tests }); expect(await this.token.delegates(holder)).to.be.equal(holder); -<<<<<<< HEAD expect(await this.token.getVotes(holder)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); }); - it('delegation without tokenId', async function () { -======= - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); - }); - - it('delegation without balance', async function () { ->>>>>>> creating permit tests + it('delegation without tokens', async function () { expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -183,11 +152,7 @@ contract('ERC721Votes', function (accounts) { }}); beforeEach(async function () { -<<<<<<< HEAD - await this.token.mint(delegatorAddress, initalTokenId); -======= - await this.token.mint(delegatorAddress, supply); ->>>>>>> creating permit tests + await this.token.mint(delegatorAddress, NFT0); }); it('accept signed delegation', async function () { @@ -211,26 +176,15 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: delegatorAddress, previousBalance: '0', -<<<<<<< HEAD newBalance: '1', -======= - newBalance: supply, ->>>>>>> creating permit tests }); expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); -<<<<<<< HEAD expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1'); -======= - expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); ->>>>>>> creating permit tests }); it('rejects reused signature', async function () { @@ -304,11 +258,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { -<<<<<<< HEAD - await this.token.mint(holder, initalTokenId); -======= - await this.token.mint(holder, supply); ->>>>>>> creating permit tests + await this.token.mint(holder, NFT0); await this.token.delegate(holder, { from: holder }); }); @@ -323,61 +273,35 @@ contract('ERC721Votes', function (accounts) { }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, -<<<<<<< HEAD previousBalance: '1', -======= - previousBalance: supply, ->>>>>>> creating permit tests newBalance: '0', }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holderDelegatee, previousBalance: '0', -<<<<<<< HEAD newBalance: '1', -======= - newBalance: supply, ->>>>>>> creating permit tests }); expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); -<<<<<<< HEAD expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); -======= - expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); ->>>>>>> creating permit tests }); }); describe('transfers', function () { beforeEach(async function () { -<<<<<<< HEAD - await this.token.mint(holder, initalTokenId); - }); - - it('no delegation', async function () { - const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); -======= - await this.token.mint(holder, supply); + await this.token.mint(holder, NFT0); }); it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); ->>>>>>> creating permit tests + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -387,37 +311,22 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); -<<<<<<< HEAD - const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); -======= - 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) }); ->>>>>>> creating permit tests const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); expect(receipt.logs.filter(({ event }) => event == 'DelegateVotesChanged').every(({ logIndex }) => transferLogIndex < logIndex)).to.be.equal(true); -<<<<<<< HEAD this.holderVotes = '0'; -======= - this.holderVotes = supply.subn(1); ->>>>>>> creating permit tests this.recipientVotes = '0'; }); it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); -<<<<<<< HEAD - const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); -======= - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); ->>>>>>> creating permit tests + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -431,25 +340,15 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); -<<<<<<< HEAD - const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); -======= - 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) }); ->>>>>>> creating permit tests 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); -<<<<<<< HEAD this.holderVotes = '0'; -======= - this.holderVotes = supply.subn(1); ->>>>>>> creating permit tests this.recipientVotes = '1'; }); @@ -468,40 +367,27 @@ contract('ERC721Votes', function (accounts) { // 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 () { -<<<<<<< HEAD - await this.token.mint(holder, initalTokenId); + await this.token.mint(holder, NFT0); await this.token.mint(holder, NFT1); await this.token.mint(holder, NFT2); await this.token.mint(holder, NFT3); -======= - await this.token.mint(holder, supply); ->>>>>>> creating permit tests }); describe('balanceOf', function () { it('grants to initial account', async function () { -<<<<<<< HEAD expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('4'); -======= - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); ->>>>>>> creating permit tests }); }); describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { -<<<<<<< HEAD await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); -======= - await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability ->>>>>>> creating permit tests expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); -<<<<<<< HEAD const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); @@ -540,47 +426,6 @@ contract('ERC721Votes', function (accounts) { const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); -======= - - const t2 = await this.token.transfer(other2, 10, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - - const t3 = await this.token.transfer(other2, 10, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '100' ]); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '90' ]); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '80' ]); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); - - await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); - }); - - it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, '100', { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - - const [ t1, t2, t3 ] = await batchInBlock([ - () => this.token.delegate(other1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - ]); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); - // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); ->>>>>>> creating permit tests }); }); @@ -601,13 +446,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); -<<<<<<< HEAD expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('4'); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); -======= - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); ->>>>>>> creating permit tests }); it('returns zero if < first checkpoint block', async function () { @@ -617,7 +457,6 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); -<<<<<<< HEAD expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); }); @@ -646,86 +485,44 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3'); expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); -======= - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 20, { from: other2 }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); ->>>>>>> creating permit tests }); }); }); - describe('getPastTotalSupply', function () { + describe('getPastVotingPower', 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), + this.token.getPastVotingPower(5e10), 'ERC721Votes: block not yet mined', ); }); it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(0)).to.be.bignumber.equal('0'); }); it('returns the latest block if >= last checkpoint block', async function () { -<<<<<<< HEAD - t1 = await this.token.mint(holder, initalTokenId); -======= - t1 = await this.token.mint(holder, supply); ->>>>>>> creating permit tests + t1 = await this.token.mint(holder, NFT0); await time.advanceBlock(); await time.advanceBlock(); -<<<<<<< HEAD - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); -======= - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); ->>>>>>> creating permit tests + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('returns zero if < first checkpoint block', async function () { await time.advanceBlock(); -<<<<<<< HEAD - const t1 = await this.token.mint(holder, initalTokenId); -======= - const t1 = await this.token.mint(holder, supply); ->>>>>>> creating permit tests + const t1 = await this.token.mint(holder, NFT0); await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); -<<<<<<< HEAD - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { @@ -742,47 +539,20 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); const t5 = await this.token.mint(holder, NFT3); -======= - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.mint(holder, supply); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.burn(10); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.burn(10); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.mint(holder, 20); ->>>>>>> creating permit tests await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); -<<<<<<< HEAD - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); -======= - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); ->>>>>>> creating permit tests + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); }); }); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js index 7a7b8cf5d83..f22c904c724 100644 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -50,7 +50,7 @@ contract('ERC721Permit', function (accounts) { ); }); - describe.only('permit', function () { + describe('permit', function () { const wallet = Wallet.generate(); const owner = wallet.getAddressString(); From 3c63a1d181b236512d4395a021f940215ace2922 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:20:14 -0400 Subject: [PATCH 113/300] Updating ERC721Vote tests --- .../token/ERC721/extensions/ERC721Votes.sol | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 6a7ef27dbec..38285ed9832 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -199,9 +199,8 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _afterTokenTransfer( address from, - address to, - uint256 tokenId - ) internal virtual override{ + address to + ) internal virtual { _moveVotingPower(delegates(from), delegates(to), 1); } @@ -287,4 +286,17 @@ abstract contract ERC721Votes is ERC721, EIP712 { function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } + + /** + * @dev Moves token from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 tokenId) external returns (bool){ + _transfer(_msgSender(), recipient, tokenId); + _afterTokenTransfer(_msgSender(), recipient); + return true; + } } From b42d0ab3d10f6b9ff9d5991b88fa42578b3f6148 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 16:30:31 -0400 Subject: [PATCH 114/300] Renaming supplied tokenId on tests --- .../ERC721/extensions/draft-ERC721Permit.sol | 87 ------------------- .../ERC721/extensions/draft-IERC721Permit.sol | 60 ------------- 2 files changed, 147 deletions(-) delete mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol delete mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol deleted file mode 100644 index c00d15367ab..00000000000 --- a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) - -pragma solidity ^0.8.0; - -import "./draft-IERC721Permit.sol"; -import "./ERC721Enumerable.sol"; -import "../../../utils/cryptography/draft-EIP712.sol"; -import "../../../utils/cryptography/ECDSA.sol"; -import "../../../utils/Counters.sol"; - -/** - * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - * - * _Available since v3.4._ - */ -abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { - using Counters for Counters.Counter; - - mapping(address => Counters.Counter) private _nonces; - - // solhint-disable-next-line var-name-mixedcase - bytes32 private immutable _PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - - /** - * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. - * - * It's a good idea to use the same `name` that is defined as the ERC721 token name. - */ - constructor(string memory name) EIP712(name, "1") {} - - /** - * @dev See {IERC721Permit-permit}. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual override { - require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); - - bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); - - bytes32 hash = _hashTypedDataV4(structHash); - - address signer = ECDSA.recover(hash, v, r, s); - require(signer == owner, "ERC721Permit: invalid signature"); - - _approve(spender, value); - } - - /** - * @dev See {IERC721Permit-nonces}. - */ - function nonces(address owner) public view virtual override returns (uint256) { - return _nonces[owner].current(); - } - - /** - * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view override returns (bytes32) { - return _domainSeparatorV4(); - } - - /** - * @dev "Consume a nonce": return the current value and increment. - * - * _Available since v4.1._ - */ - function _useNonce(address owner) internal virtual returns (uint256 current) { - Counters.Counter storage nonce = _nonces[owner]; - current = nonce.current(); - nonce.increment(); - } -} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol deleted file mode 100644 index 61882f2de0d..00000000000 --- a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - */ -interface IERC721Permit { - /** - * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, - * given ``owner``'s signed approval. - * - * IMPORTANT: The same issues {IERC721-approve} has related to transaction - * ordering also apply here. - * - * Emits an {Approval} event. - * - * Requirements: - * - * - `spender` cannot be the zero address. - * - `deadline` must be a timestamp in the future. - * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` - * over the EIP712-formatted function arguments. - * - the signature must use ``owner``'s current nonce (see {nonces}). - * - * For more information on the signature format, see the - * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP - * section]. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - /** - * @dev Returns the current nonce for `owner`. This value must be - * included whenever a signature is generated for {permit}. - * - * Every successful call to {permit} increases ``owner``'s nonce by one. This - * prevents a signature from being used multiple times. - */ - function nonces(address owner) external view returns (uint256); - - /** - * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view returns (bytes32); -} From dc0ec55d6cd8fac52cc2c86eb4812e230bf4c70a Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 17:45:23 -0400 Subject: [PATCH 115/300] Updating tests based on new contract changes --- contracts/mocks/ERC721PermitMock.sol | 20 ------------------- contracts/mocks/ERC721VotesMock.sol | 4 ++++ .../token/ERC721/extensions/ERC721Votes.sol | 19 +++++++++++++++++- 3 files changed, 22 insertions(+), 21 deletions(-) delete mode 100644 contracts/mocks/ERC721PermitMock.sol diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol deleted file mode 100644 index 37a860ef4dc..00000000000 --- a/contracts/mocks/ERC721PermitMock.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../token/ERC721/extensions/draft-ERC721Permit.sol"; - -contract ERC721PermitMock is ERC721Permit { - constructor( - string memory name, - string memory symbol, - address initialAccount, - uint256 tokenId - ) payable ERC721(name, symbol) ERC721Permit(name) { - _mint(initialAccount, tokenId); - } - - function getChainId() external view returns (uint256) { - return block.chainid; - } -} diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index bde65e5a5ff..869dc27d6e3 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,7 +5,11 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { +<<<<<<< HEAD constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} +======= + constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {} +>>>>>>> Updating tests based on new contract changes function mint(address account, uint256 tokenId) public { _mint(account, tokenId); diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 38285ed9832..1e4e9a308d8 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -8,6 +8,7 @@ import "../../../utils/Counters.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; /** * @dev Extension of ERC721 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. @@ -43,7 +44,12 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. * * It's a good idea to use the same `name` that is defined as the ERC721 token name. +<<<<<<< HEAD */ +======= + + constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ +>>>>>>> Updating tests based on new contract changes /** * @dev Emitted when an account changes their delegate. @@ -179,7 +185,8 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _mint(address account, uint256 tokenId) internal virtual override { super._mint(account, tokenId); - require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + _totalSupply += 1; + require(_totalSupply <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } @@ -189,6 +196,10 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); +<<<<<<< HEAD +======= + _totalSupply -= 1; +>>>>>>> Updating tests based on new contract changes _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); } @@ -199,8 +210,14 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _afterTokenTransfer( address from, +<<<<<<< HEAD address to ) internal virtual { +======= + address to, + uint256 tokenId + ) internal virtual override{ +>>>>>>> Updating tests based on new contract changes _moveVotingPower(delegates(from), delegates(to), 1); } From 9d0f7fe85678f3ce1b85499821e28d7e3df2ec75 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 20:15:54 -0400 Subject: [PATCH 116/300] Updating execution order inside of mint --- contracts/token/ERC721/extensions/ERC721Votes.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 1e4e9a308d8..eaf65ef2839 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -184,10 +184,11 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been increased. */ function _mint(address account, uint256 tokenId) internal virtual override { + require(_totalSupply+1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + super._mint(account, tokenId); _totalSupply += 1; - require(_totalSupply <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } From 9efd8c1d48f62061e31214ec1affd570480b0412 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 16:25:14 -0400 Subject: [PATCH 117/300] Adding Mocks for testing and integrating nft minting to current workflow test --- .../extensions/GovernorERC721.test.js | 55 +++++++++++++------ 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index ee8ad8399da..11a0f382b28 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -1,4 +1,5 @@ -const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); +const { BN } = require('bn.js'); const Enums = require('../../helpers/enums'); const { @@ -15,21 +16,35 @@ contract('GovernorERC721Mock', function (accounts) { const name = 'OZ-Governor'; const tokenName = 'MockNFToken'; const tokenSymbol = 'MTKN'; - const initalTokenId = web3.utils.toWei('100'); + const NFT0 = web3.utils.toWei('100'); const NFT1 = web3.utils.toWei('10'); const NFT2 = web3.utils.toWei('20'); const NFT3 = web3.utils.toWei('30'); + const NFT4 = web3.utils.toWei('40'); + + // Must be the same as in contract + const ProposalState = { + Pending: new BN('0'), + Active: new BN('1'), + Canceled: new BN('2'), + Defeated: new BN('3'), + Succeeded: new BN('4'), + Queued: new BN('5'), + Expired: new BN('6'), + Executed: new BN('7'), + }; beforeEach(async function () { this.owner = owner; this.token = await Token.new(tokenName, tokenSymbol); this.mock = await Governor.new(name, this.token.address); this.receiver = await CallReceiver.new(); - await this.token.mint(owner, initalTokenId); + await this.token.mint(owner, NFT0); await this.token.mint(owner, NFT1); await this.token.mint(owner, NFT2); await this.token.mint(owner, NFT3); - + await this.token.mint(owner, NFT4); + await this.token.delegate(voter1, { from: voter1 }); await this.token.delegate(voter2, { from: voter2 }); await this.token.delegate(voter3, { from: voter3 }); @@ -55,20 +70,20 @@ contract('GovernorERC721Mock', function (accounts) { ], tokenHolder: owner, voters: [ - { voter: voter1, nftWeight: initalTokenId, support: Enums.VoteType.For }, - { voter: voter2, nftWeight: NFT1, support: Enums.VoteType.For }, - { voter: voter3, nftWeight: NFT2, support: Enums.VoteType.Against }, - { voter: voter4, nftWeight: NFT3, support: Enums.VoteType.Abstain }, - ] - } + { voter: voter1, nfts: [NFT0], support: Enums.VoteType.For }, + { voter: voter2, nfts: [NFT1, NFT2], support: Enums.VoteType.For }, + { voter: voter3, nfts: [NFT3], support: Enums.VoteType.Against }, + { voter: voter4, nfts: [NFT4], support: Enums.VoteType.Abstain }, + ], + }; }); afterEach(async function () { - expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); - for(const vote of this.receipts.castVote.filter(Boolean)){ + for (const vote of this.receipts.castVote.filter(Boolean)) { const { voter } = vote.logs.find(Boolean).args; - + expect(await this.mock.hasVoted(this.id, voter)).to.be.equal(true); expectEvent( @@ -77,19 +92,27 @@ contract('GovernorERC721Mock', function (accounts) { this.settings.voters.find(({ address }) => address === voter), ); - expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); + if (voter === voter2) { + expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('2'); + } else { + expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); + } } await this.mock.proposalVotes(this.id).then(result => { for (const [key, value] of Object.entries(Enums.VoteType)) { expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( - Object.values(this.settings.voters).filter(({ support }) => support === value).length.toString() + Object.values(this.settings.voters).filter(({ support }) => support === value).reduce( + (acc, { nfts }) => acc.add(new BN(nfts.length)), + new BN('0'), + ), ); } }); + + expect(await this.mock.state(this.id)).to.be.bignumber.equal(ProposalState.Executed); }); runGovernorWorkflow(); - }); }); From 87cf640e394d96703ff16f89b1d7eb277bc5addf Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 18:57:08 -0400 Subject: [PATCH 118/300] Governance adocs update --- contracts/mocks/ERC721VotesMock.sol | 12 +++---- contracts/mocks/UserTOkenerc721Mock.sol | 32 +++++++++++++++++++ .../token/ERC721/extensions/ERC721Votes.sol | 4 +++ 3 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 869dc27d6e3..b47bfd8f24e 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -2,14 +2,10 @@ pragma solidity ^0.8.0; -import "../token/ERC721/extensions/ERC721Votes.sol"; +import "../token/ERC721/extensions/draft-ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { -<<<<<<< HEAD - constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} -======= - constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {} ->>>>>>> Updating tests based on new contract changes + constructor(string memory name, string memory symbol) ERC721Votes(name, symbol) {} function mint(address account, uint256 tokenId) public { _mint(account, tokenId); @@ -23,7 +19,7 @@ contract ERC721VotesMock is ERC721Votes { return block.chainid; } - function _maxSupply() internal pure override returns(uint224){ - return uint224(4); + function _maxSupply() internal pure override returns (uint224) { + return uint224(5); } } diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol new file mode 100644 index 00000000000..b3874e66da5 --- /dev/null +++ b/contracts/mocks/UserTOkenerc721Mock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; + +contract MyToken is ERC721, EIP712, ERC721Votes { + constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._burn(account, amount); + } +} \ No newline at end of file diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index eaf65ef2839..819cedba82d 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -44,12 +44,16 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. * * It's a good idea to use the same `name` that is defined as the ERC721 token name. +<<<<<<< HEAD <<<<<<< HEAD */ ======= constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ >>>>>>> Updating tests based on new contract changes +======= + */ +>>>>>>> Governance adocs update /** * @dev Emitted when an account changes their delegate. From b135af50ec3764dfd128c3c81f7165754d576c9b Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 14:46:27 -0400 Subject: [PATCH 119/300] Updating contracts listing order --- docs/modules/ROOT/pages/governance.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index a262a75af38..6014f778dc1 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -14,14 +14,14 @@ This governance protocol is generally implemented in a special-purpose contract OpenZeppelin’s Governor system was designed with a concern for compatibility with existing systems that were based on Compound’s GovernorAlpha and GovernorBravo. Because of this, you will find that many modules are presented in two variants, one of which is built for compatibility with those systems. -=== ERC721Votes - -The ERC721 extension to keep track of votes and vote delegation is one such case. - === ERC20Votes & ERC20VotesComp The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. +=== ERC721Votes + +The ERC721 extension to keep track of votes and vote delegation is one such case. + === Governor & GovernorCompatibilityBravo An OpenZeppelin Governor contract is by default not interface-compatible with GovernorAlpha or Bravo, since some of the functions are different or missing, although it shares all of the same events. However, it’s possible to opt in to full compatibility by inheriting from the GovernorCompatibilityBravo module. The contract will be cheaper to deploy and use without this module. From 510c373abbbaef13f516d988c6ded70d448a4f16 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 15:02:56 -0400 Subject: [PATCH 120/300] Following lint suggestions --- test/governance/GovernorWorkflow.behavior.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js index e8e2416b19e..ae178d7c9de 100644 --- a/test/governance/GovernorWorkflow.behavior.js +++ b/test/governance/GovernorWorkflow.behavior.js @@ -31,9 +31,10 @@ function runGovernorWorkflow () { for (const voter of this.settings.voters) { if (voter.weight) { await this.token.transfer(voter.voter, voter.weight, { from: this.settings.tokenHolder }); - }else if(voter.nftWeight){ - await this.token.transferFrom(this.settings.tokenHolder, voter.voter, voter.nftWeight, { from: this.settings.tokenHolder }); - } + } else if (voter.nftWeight) { + await this.token.transferFrom(this.settings.tokenHolder, voter.voter, voter.nftWeight, + { from: this.settings.tokenHolder }); + } } } From f58c91e50e75ff1ea74061675e1801ac5a8ca35d Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 15:18:09 -0400 Subject: [PATCH 121/300] Delete of test contract --- contracts/mocks/UserTOkenerc721Mock.sol | 32 ------------------------- 1 file changed, 32 deletions(-) delete mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol deleted file mode 100644 index b3874e66da5..00000000000 --- a/contracts/mocks/UserTOkenerc721Mock.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "@openzeppelin/contracts/token/ERC721.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; - -contract MyToken is ERC721, EIP712, ERC721Votes { - constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._afterTokenTransfer(from, to, amount); - } - - function _mint(address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._mint(to, amount); - } - - function _burn(address account, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._burn(account, amount); - } -} \ No newline at end of file From 8663d8406804c8e1682041bf982c86e38eebe959 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sat, 30 Oct 2021 20:49:29 -0400 Subject: [PATCH 122/300] Initial contracts creation --- .../token/ERC721/extensions/ERC721Votes.sol | 324 ------------------ 1 file changed, 324 deletions(-) delete mode 100644 contracts/token/ERC721/extensions/ERC721Votes.sol diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol deleted file mode 100644 index 819cedba82d..00000000000 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ /dev/null @@ -1,324 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/ERC721Votes.sol) - -pragma solidity ^0.8.0; - -import "../ERC721.sol"; -import "../../../utils/Counters.sol"; -import "../../../utils/math/Math.sol"; -import "../../../utils/math/SafeCast.sol"; -import "../../../utils/cryptography/ECDSA.sol"; -import "../../../utils/cryptography/draft-EIP712.sol"; -/** - * @dev Extension of ERC721 to support Compound-like voting and delegation. This version is more generic than Compound's, - * and supports token supply up to 2^224^ - 1, while COMP is limited to 2^96^ - 1. - * - * This 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 {getVotes} and {getPastVotes}. - * - * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it - * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. - * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this - * will significantly increase the base gas cost of transfers. - * - * _Available since v4.2._ - */ -abstract contract ERC721Votes is ERC721, EIP712 { - using Counters for Counters.Counter; - - struct Checkpoint { - uint32 fromBlock; - uint224 votes; - } - uint256 _totalSupply; - bytes32 private constant _DELEGATION_TYPEHASH = - keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); - - mapping(address => address) private _delegates; - mapping(address => Counters.Counter) private _nonces; - mapping(address => Checkpoint[]) private _checkpoints; - Checkpoint[] private _totalSupplyCheckpoints; - - /** - * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. - * - * It's a good idea to use the same `name` that is defined as the ERC721 token name. -<<<<<<< HEAD -<<<<<<< HEAD - */ -======= - - constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ ->>>>>>> Updating tests based on new contract changes -======= - */ ->>>>>>> Governance adocs update - - /** - * @dev Emitted when an account changes their delegate. - */ - event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); - - /** - * @dev Emitted when a token transfer or delegate change results in changes to an account's voting power. - */ - event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); - - /** - * @dev Get the `pos`-th checkpoint for `account`. - */ - function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoint memory) { - return _checkpoints[account][pos]; - } - - /** - * @dev Get number of checkpoints for `account`. - */ - function numCheckpoints(address account) public view virtual returns (uint32) { - return SafeCast.toUint32(_checkpoints[account].length); - } - - /** - * @dev Get the address `account` is currently delegating to. - */ - function delegates(address account) public view virtual returns (address) { - return _delegates[account]; - } - - /** - * @dev Gets the current votes balance for `account` - */ - function getVotes(address account) public view returns (uint256) { - uint256 pos = _checkpoints[account].length; - return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes; - } - - /** - * @dev Retrieve the number of votes for `account` at the end of `blockNumber`. - * - * Requirements: - * - * - `blockNumber` must have been already mined - */ - function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) { - require(blockNumber < block.number, "ERC721Votes: block not yet mined"); - return _checkpointsLookup(_checkpoints[account], blockNumber); - } - - /** - * @dev Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances. - * It is but NOT the sum of all the delegated votes! - * - * Requirements: - * - * - `blockNumber` must have been already mined - */ - function getPastTotalSupply(uint256 blockNumber) public view returns (uint256) { - require(blockNumber < block.number, "ERC721Votes: block not yet mined"); - return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber); - } - - /** - * @dev Lookup a value in a list of (sorted) checkpoints. - */ - function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) private view returns (uint256) { - // We run a binary search to look for the earliest checkpoint taken after `blockNumber`. - // - // During the loop, the index of the wanted checkpoint remains in the range [low-1, high). - // With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant. - // - If the middle checkpoint is after `blockNumber`, we look in [low, mid) - // - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high) - // Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not - // out of bounds (in which case we're looking too far in the past and the result is 0). - // Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is - // past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out - // the same. - uint256 high = ckpts.length; - uint256 low = 0; - while (low < high) { - uint256 mid = Math.average(low, high); - if (ckpts[mid].fromBlock > blockNumber) { - high = mid; - } else { - low = mid + 1; - } - } - - return high == 0 ? 0 : ckpts[high - 1].votes; - } - - /** - * @dev Delegate votes from the sender to `delegatee`. - */ - function delegate(address delegatee) public virtual { - _delegate(_msgSender(), delegatee); - } - - /** - * @dev Delegates votes from signer to `delegatee` - */ - function delegateBySig( - address delegatee, - uint256 nonce, - uint256 expiry, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual { - require(block.timestamp <= expiry, "ERC721Votes: signature expired"); - address signer = ECDSA.recover( - _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), - v, - r, - s - ); - require(nonce == _useNonce(signer), "ERC721Votes: invalid nonce"); - _delegate(signer, delegatee); - } - - /** - * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). - */ - function _maxSupply() internal view virtual returns (uint224) { - return type(uint224).max; - } - - /** - * @dev Snapshots the totalSupply after it has been increased. - */ - function _mint(address account, uint256 tokenId) internal virtual override { - require(_totalSupply+1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - - super._mint(account, tokenId); - _totalSupply += 1; - - _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); - } - - /** - * @dev Snapshots the totalSupply after it has been decreased. - */ - function _burn(uint256 tokenId) internal virtual override { - super._burn(tokenId); -<<<<<<< HEAD -======= - _totalSupply -= 1; ->>>>>>> Updating tests based on new contract changes - _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); - } - - /** - * @dev Move voting power when tokens are transferred. - * - * Emits a {DelegateVotesChanged} event. - */ - function _afterTokenTransfer( - address from, -<<<<<<< HEAD - address to - ) internal virtual { -======= - address to, - uint256 tokenId - ) internal virtual override{ ->>>>>>> Updating tests based on new contract changes - _moveVotingPower(delegates(from), delegates(to), 1); - } - - /** - * @dev Change delegation for `delegator` to `delegatee`. - * - * Emits events {DelegateChanged} and {DelegateVotesChanged}. - */ - function _delegate(address delegator, address delegatee) internal virtual { - address currentDelegate = delegates(delegator); - uint256 delegatorBalance = balanceOf(delegator); - _delegates[delegator] = delegatee; - - emit DelegateChanged(delegator, currentDelegate, delegatee); - - _moveVotingPower(currentDelegate, delegatee, delegatorBalance); - } - - function _moveVotingPower( - address src, - address dst, - uint256 amount - ) private { - if (src != dst && amount > 0) { - if (src != address(0)) { - (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[src], _subtract, amount); - emit DelegateVotesChanged(src, oldWeight, newWeight); - } - - if (dst != address(0)) { - (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[dst], _add, amount); - emit DelegateVotesChanged(dst, oldWeight, newWeight); - } - } - } - - function _writeCheckpoint( - Checkpoint[] storage ckpts, - function(uint256, uint256) view returns (uint256) op, - uint256 delta - ) private returns (uint256 oldWeight, uint256 newWeight) { - uint256 pos = ckpts.length; - oldWeight = pos == 0 ? 0 : ckpts[pos - 1].votes; - newWeight = op(oldWeight, delta); - - if (pos > 0 && ckpts[pos - 1].fromBlock == block.number) { - ckpts[pos - 1].votes = SafeCast.toUint224(newWeight); - } else { - ckpts.push(Checkpoint({fromBlock: SafeCast.toUint32(block.number), votes: SafeCast.toUint224(newWeight)})); - } - } - - /** - * @dev "Consume a nonce": return the current value and increment. - * - * _Available since v4.1._ - */ - function _useNonce(address owner) internal virtual returns (uint256 current) { - Counters.Counter storage nonce = _nonces[owner]; - current = nonce.current(); - nonce.increment(); - } - - /** - * @dev Returns an address nonce. - */ - function nonces(address owner) public view virtual returns (uint256) { - return _nonces[owner].current(); - } - - /** - * @dev Returns DOMAIN_SEPARATOR. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view returns (bytes32) { - return _domainSeparatorV4(); - } - - function _add(uint256 a, uint256 b) private pure returns (uint256) { - return a + b; - } - - function _subtract(uint256 a, uint256 b) private pure returns (uint256) { - return a - b; - } - - /** - * @dev Moves token from the caller's account to `recipient`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 tokenId) external returns (bool){ - _transfer(_msgSender(), recipient, tokenId); - _afterTokenTransfer(_msgSender(), recipient); - return true; - } -} From 6d69091737f11e350deb3492588ae39a6cee0e71 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 09:28:35 -0400 Subject: [PATCH 123/300] creating permit tests --- contracts/mocks/ERC721PermitMock.sol | 20 +++ .../ERC721/extensions/ERC721Votes.test.js | 22 ++-- .../extensions/draft-ERC721Permit.test.js | 117 ------------------ 3 files changed, 31 insertions(+), 128 deletions(-) create mode 100644 contracts/mocks/ERC721PermitMock.sol delete mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol new file mode 100644 index 00000000000..37a860ef4dc --- /dev/null +++ b/contracts/mocks/ERC721PermitMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC721/extensions/draft-ERC721Permit.sol"; + +contract ERC721PermitMock is ERC721Permit { + constructor( + string memory name, + string memory symbol, + address initialAccount, + uint256 tokenId + ) payable ERC721(name, symbol) ERC721Permit(name) { + _mint(initialAccount, tokenId); + } + + function getChainId() external view returns (uint256) { + return block.chainid; + } +} diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 81397dda8aa..0ef80342a7d 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -382,13 +382,12 @@ contract('ERC721Votes', function (accounts) { describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { - await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - + const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); @@ -398,10 +397,10 @@ contract('ERC721Votes', function (accounts) { const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); + expect(await this.token.checkpointAt(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); + expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); + expect(await this.token.checkpointAt(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); + expect(await this.token.checkpointAt(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); await time.advanceBlock(); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('2'); @@ -420,12 +419,13 @@ contract('ERC721Votes', function (accounts) { () => this.token.transferFrom(recipient, other2, NFT1, { from: recipient, gas: 200000 }), () => this.token.transferFrom(recipient, other2, NFT2, { from: recipient, gas: 200000 }), ]); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); + expect(await this.token.checkpointAt(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); + expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); }); @@ -433,7 +433,7 @@ contract('ERC721Votes', function (accounts) { it('reverts if block number >= current block', async function () { await expectRevert( this.token.getPastVotes(other1, 5e10), - 'ERC721Votes: block not yet mined', + 'block not yet mined', ); }); @@ -541,7 +541,7 @@ contract('ERC721Votes', function (accounts) { const t5 = await this.token.mint(holder, NFT3); await time.advanceBlock(); await time.advanceBlock(); - + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js deleted file mode 100644 index f22c904c724..00000000000 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable */ - -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); -const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; - -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; - -const ERC721PermitMock = artifacts.require('ERC721PermitMock'); - -const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); - -const Permit = [ - { name: 'owner', type: 'address' }, - { name: 'spender', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint256' }, -]; - -contract('ERC721Permit', function (accounts) { - const [ initialHolder, spender, recipient, other ] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - - const initialTokenId = new BN('100'); - - beforeEach(async function () { - this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); - - // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id - // from within the EVM as from the JSON RPC interface. - // See https://github.com/trufflesuite/ganache-core/issues/515 - this.chainId = await this.token.getChainId(); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect( - await this.token.DOMAIN_SEPARATOR(), - ).to.equal( - await domainSeparator(name, version, this.chainId, this.token.address), - ); - }); - - describe('permit', function () { - const wallet = Wallet.generate(); - - const owner = wallet.getAddressString(); - const value = initialTokenId; - const nonce = 0; - const maxDeadline = MAX_UINT256; - - const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ - primaryType: 'Permit', - types: { EIP712Domain, Permit }, - domain: { name, version, chainId, verifyingContract }, - message: { owner, spender, value, nonce, deadline }, - }); - - it('accepts owner signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); - expect(await this.token.getApproved(value)).to.be.equal(spender); - }); - - it('rejects reused signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects other signature', async function () { - const otherWallet = Wallet.generate(); - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects expired permit', async function () { - const deadline = (await time.latest()) - time.duration.weeks(1); - - const data = buildData(this.chainId, this.token.address, deadline); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, deadline, v, r, s), - 'ERC721Permit: expired deadline', - ); - }); - }); -}); From 72c887dd2811259afb53592f10134e142d0791ef Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 10:26:22 -0400 Subject: [PATCH 124/300] Fixing checkpoints count --- contracts/token/ERC20/extensions/ERC20Votes.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index f4fd21d3e5b..dcbaf972220 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -7,7 +7,6 @@ import "./draft-ERC20Permit.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.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. @@ -174,7 +173,7 @@ abstract contract ERC20Votes is ERC20Permit { super._mint(account, amount); require(totalSupply() <= _maxSupply(), "ERC20Votes: total supply risks overflowing votes"); - _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } /** From b94e4759a165a751f70aeeecf8742cc153531024 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:20:14 -0400 Subject: [PATCH 125/300] Updating ERC721Vote tests --- contracts/mocks/ERC721VotesMock.sol | 5 + .../token/ERC20/extensions/ERC20Votes.sol | 13 +++ .../ERC721/extensions/ERC721Votes.test.js | 107 +++++++++++++++++- 3 files changed, 124 insertions(+), 1 deletion(-) diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index b47bfd8f24e..bb9bca5b9f2 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -19,7 +19,12 @@ contract ERC721VotesMock is ERC721Votes { return block.chainid; } +<<<<<<< HEAD function _maxSupply() internal pure override returns (uint224) { return uint224(5); +======= + function _maxSupply() internal pure override returns(uint224){ + return uint224(4); +>>>>>>> Updating ERC721Vote tests } } diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index dcbaf972220..7313da275f1 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -256,4 +256,17 @@ abstract contract ERC20Votes is ERC20Permit { function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } + + /** + * @dev Moves token from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 tokenId) external returns (bool){ + _transfer(_msgSender(), recipient, tokenId); + _afterTokenTransfer(_msgSender(), recipient); + return true; + } } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 0ef80342a7d..b136f411c4b 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -14,6 +14,7 @@ const queue = promisify(setImmediate); const ERC721VotesMock = artifacts.require('ERC721VotesMock'); const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); +const { Console } = require('console'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -89,8 +90,12 @@ contract('ERC721Votes', function (accounts) { this.token.mint(holder, NFT1); this.token.mint(holder, NFT2); this.token.mint(holder, NFT3); +<<<<<<< HEAD this.token.mint(holder, NFT0); this.token.mint(holder, NFT4); +======= + this.token.mint(holder, supply); +>>>>>>> Updating ERC721Vote tests await expectRevert( this.token.mint(holder, lastTokenId), @@ -258,7 +263,11 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { +<<<<<<< HEAD await this.token.mint(holder, NFT0); +======= + await this.token.mint(holder, supply); +>>>>>>> Updating ERC721Vote tests await this.token.delegate(holder, { from: holder }); }); @@ -300,8 +309,13 @@ contract('ERC721Votes', function (accounts) { }); it('no delegation', async function () { +<<<<<<< HEAD const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); +======= + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); +>>>>>>> Updating ERC721Vote tests expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -311,8 +325,13 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); +<<<<<<< HEAD const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); +======= + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); +>>>>>>> Updating ERC721Vote tests expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -325,8 +344,13 @@ contract('ERC721Votes', function (accounts) { it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); +<<<<<<< HEAD const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); +======= + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); +>>>>>>> Updating ERC721Vote tests expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -340,8 +364,13 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); +<<<<<<< HEAD const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); +======= + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); +>>>>>>> Updating ERC721Vote tests expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -381,6 +410,13 @@ contract('ERC721Votes', function (accounts) { describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { +<<<<<<< HEAD +======= + + await this.token.transfer(recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transfer(recipient, NFT2, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); +>>>>>>> Updating ERC721Vote tests await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); @@ -388,6 +424,7 @@ contract('ERC721Votes', function (accounts) { const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); +<<<<<<< HEAD const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); @@ -401,6 +438,22 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); expect(await this.token.checkpointAt(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); expect(await this.token.checkpointAt(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); +======= + + const t2 = await this.token.transfer(other2, NFT1, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + + const t3 = await this.token.transfer(other2, NFT2, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); + + const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); + + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); + expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); + expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); +>>>>>>> Updating ERC721Vote tests await time.advanceBlock(); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('2'); @@ -410,6 +463,7 @@ contract('ERC721Votes', function (accounts) { }); it('does not add more than one checkpoint in a block', async function () { +<<<<<<< HEAD await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); @@ -418,12 +472,28 @@ contract('ERC721Votes', function (accounts) { () => this.token.delegate(other1, { from: recipient, gas: 200000 }), () => this.token.transferFrom(recipient, other2, NFT1, { from: recipient, gas: 200000 }), () => this.token.transferFrom(recipient, other2, NFT2, { from: recipient, gas: 200000 }), +======= + await this.token.transfer(recipient, NFT1, { from: holder }); + await this.token.transfer(recipient, NFT2, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const [ t1, t2, t3 ] = await batchInBlock([ + () => this.token.delegate(other1, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, NFT1, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, NFT2, { from: recipient, gas: 100000 }), +>>>>>>> Updating ERC721Vote tests ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); +<<<<<<< HEAD expect(await this.token.checkpointAt(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); +======= + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); + + const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); +>>>>>>> Updating ERC721Vote tests expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); @@ -466,6 +536,7 @@ contract('ERC721Votes', function (accounts) { const t1 = await this.token.delegate(other1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); +<<<<<<< HEAD const t2 = await this.token.transferFrom(holder, other2, NFT1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); @@ -473,6 +544,15 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); const t4 = await this.token.transferFrom(other2, holder, NFT2, { from: other2 }); +======= + const t2 = await this.token.transfer(other2, NFT1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.transfer(other2, NFT2, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.transfer(holder, NFT2, { from: other2 }); +>>>>>>> Updating ERC721Vote tests await time.advanceBlock(); await time.advanceBlock(); @@ -491,7 +571,7 @@ contract('ERC721Votes', function (accounts) { describe('getPastVotingPower', function () { beforeEach(async function () { - await this.token.delegate(holder, { from: holder }); + // await this.token.delegate(holder, { from: holder }); }); it('reverts if block number >= current block', async function () { @@ -511,8 +591,13 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); +<<<<<<< HEAD expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); +======= + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); +>>>>>>> Updating ERC721Vote tests }); it('returns zero if < first checkpoint block', async function () { @@ -521,8 +606,13 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); +<<<<<<< HEAD expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); +======= + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); +>>>>>>> Updating ERC721Vote tests }); it('generally returns the voting balance at the appropriate checkpoint', async function () { @@ -541,6 +631,7 @@ contract('ERC721Votes', function (accounts) { const t5 = await this.token.mint(holder, NFT3); await time.advanceBlock(); await time.advanceBlock(); +<<<<<<< HEAD expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); @@ -553,6 +644,20 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.getPastVotingPower(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastVotingPower(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotingPower(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); +======= + + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); +>>>>>>> Updating ERC721Vote tests }); }); }); From a7397bafd63bbe620748707a31ebaccae9c2088e Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:23:31 -0400 Subject: [PATCH 126/300] Updating ERC721Vote tests descriptions --- .../ERC721/extensions/ERC721Votes.test.js | 107 +----------------- 1 file changed, 1 insertion(+), 106 deletions(-) diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index b136f411c4b..0ef80342a7d 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -14,7 +14,6 @@ const queue = promisify(setImmediate); const ERC721VotesMock = artifacts.require('ERC721VotesMock'); const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); -const { Console } = require('console'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -90,12 +89,8 @@ contract('ERC721Votes', function (accounts) { this.token.mint(holder, NFT1); this.token.mint(holder, NFT2); this.token.mint(holder, NFT3); -<<<<<<< HEAD this.token.mint(holder, NFT0); this.token.mint(holder, NFT4); -======= - this.token.mint(holder, supply); ->>>>>>> Updating ERC721Vote tests await expectRevert( this.token.mint(holder, lastTokenId), @@ -263,11 +258,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { -<<<<<<< HEAD await this.token.mint(holder, NFT0); -======= - await this.token.mint(holder, supply); ->>>>>>> Updating ERC721Vote tests await this.token.delegate(holder, { from: holder }); }); @@ -309,13 +300,8 @@ contract('ERC721Votes', function (accounts) { }); it('no delegation', async function () { -<<<<<<< HEAD const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); -======= - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); ->>>>>>> Updating ERC721Vote tests expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -325,13 +311,8 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); -<<<<<<< HEAD const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); -======= - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); ->>>>>>> Updating ERC721Vote tests expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -344,13 +325,8 @@ contract('ERC721Votes', function (accounts) { it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); -<<<<<<< HEAD const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); -======= - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); ->>>>>>> Updating ERC721Vote tests expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -364,13 +340,8 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); -<<<<<<< HEAD const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); -======= - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); ->>>>>>> Updating ERC721Vote tests expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -410,13 +381,6 @@ contract('ERC721Votes', function (accounts) { describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { -<<<<<<< HEAD -======= - - await this.token.transfer(recipient, NFT1, { from: holder }); //give an account two tokens for readability - await this.token.transfer(recipient, NFT2, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); ->>>>>>> Updating ERC721Vote tests await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); @@ -424,7 +388,6 @@ contract('ERC721Votes', function (accounts) { const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); -<<<<<<< HEAD const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); @@ -438,22 +401,6 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); expect(await this.token.checkpointAt(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); expect(await this.token.checkpointAt(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); -======= - - const t2 = await this.token.transfer(other2, NFT1, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - - const t3 = await this.token.transfer(other2, NFT2, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - - const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); ->>>>>>> Updating ERC721Vote tests await time.advanceBlock(); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('2'); @@ -463,7 +410,6 @@ contract('ERC721Votes', function (accounts) { }); it('does not add more than one checkpoint in a block', async function () { -<<<<<<< HEAD await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); @@ -472,28 +418,12 @@ contract('ERC721Votes', function (accounts) { () => this.token.delegate(other1, { from: recipient, gas: 200000 }), () => this.token.transferFrom(recipient, other2, NFT1, { from: recipient, gas: 200000 }), () => this.token.transferFrom(recipient, other2, NFT2, { from: recipient, gas: 200000 }), -======= - await this.token.transfer(recipient, NFT1, { from: holder }); - await this.token.transfer(recipient, NFT2, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - - const [ t1, t2, t3 ] = await batchInBlock([ - () => this.token.delegate(other1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, NFT1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, NFT2, { from: recipient, gas: 100000 }), ->>>>>>> Updating ERC721Vote tests ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); -<<<<<<< HEAD expect(await this.token.checkpointAt(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); -======= - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); - - const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); ->>>>>>> Updating ERC721Vote tests expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); @@ -536,7 +466,6 @@ contract('ERC721Votes', function (accounts) { const t1 = await this.token.delegate(other1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); -<<<<<<< HEAD const t2 = await this.token.transferFrom(holder, other2, NFT1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); @@ -544,15 +473,6 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); const t4 = await this.token.transferFrom(other2, holder, NFT2, { from: other2 }); -======= - const t2 = await this.token.transfer(other2, NFT1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.transfer(other2, NFT2, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.transfer(holder, NFT2, { from: other2 }); ->>>>>>> Updating ERC721Vote tests await time.advanceBlock(); await time.advanceBlock(); @@ -571,7 +491,7 @@ contract('ERC721Votes', function (accounts) { describe('getPastVotingPower', function () { beforeEach(async function () { - // await this.token.delegate(holder, { from: holder }); + await this.token.delegate(holder, { from: holder }); }); it('reverts if block number >= current block', async function () { @@ -591,13 +511,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); -<<<<<<< HEAD expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); -======= - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); ->>>>>>> Updating ERC721Vote tests }); it('returns zero if < first checkpoint block', async function () { @@ -606,13 +521,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); -<<<<<<< HEAD expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); -======= - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); ->>>>>>> Updating ERC721Vote tests }); it('generally returns the voting balance at the appropriate checkpoint', async function () { @@ -631,7 +541,6 @@ contract('ERC721Votes', function (accounts) { const t5 = await this.token.mint(holder, NFT3); await time.advanceBlock(); await time.advanceBlock(); -<<<<<<< HEAD expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); @@ -644,20 +553,6 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.getPastVotingPower(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastVotingPower(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotingPower(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); -======= - - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); ->>>>>>> Updating ERC721Vote tests }); }); }); From 6ed1ccde83e583be482b6fc4bd2033e71e0023ce Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 16:24:20 -0400 Subject: [PATCH 127/300] Adding _afterTokenTransfer to base ERC271 contract --- .../extensions/GovernorVotesERC721.sol | 2 +- contracts/mocks/ERC721VotesMock.sol | 5 ----- .../token/ERC20/extensions/ERC20Votes.sol | 18 +++--------------- 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index c2472a2e4e2..2e3079fc243 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC72GovernorVotesERC721.sol) +// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC721GovernorVotesERC721.sol) pragma solidity ^0.8.0; diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index bb9bca5b9f2..b47bfd8f24e 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -19,12 +19,7 @@ contract ERC721VotesMock is ERC721Votes { return block.chainid; } -<<<<<<< HEAD function _maxSupply() internal pure override returns (uint224) { return uint224(5); -======= - function _maxSupply() internal pure override returns(uint224){ - return uint224(4); ->>>>>>> Updating ERC721Vote tests } } diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index 7313da275f1..5e176973ee2 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.0 (token/ERC20/extensions/ERC20Votes.sol) +// OpenZeppelin Contracts v4.3.2 (token/ERC20/extensions/ERC20Votes.sol) pragma solidity ^0.8.0; @@ -7,6 +7,7 @@ import "./draft-ERC20Permit.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.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. @@ -173,7 +174,7 @@ abstract contract ERC20Votes is ERC20Permit { super._mint(account, amount); require(totalSupply() <= _maxSupply(), "ERC20Votes: total supply risks overflowing votes"); - _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); + _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); } /** @@ -256,17 +257,4 @@ abstract contract ERC20Votes is ERC20Permit { function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } - - /** - * @dev Moves token from the caller's account to `recipient`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 tokenId) external returns (bool){ - _transfer(_msgSender(), recipient, tokenId); - _afterTokenTransfer(_msgSender(), recipient); - return true; - } } From 718274a0e92a5d9e04d07fdaa66366a3e20f7825 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 17:45:23 -0400 Subject: [PATCH 128/300] Updating tests based on new contract changes --- contracts/mocks/ERC721PermitMock.sol | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 contracts/mocks/ERC721PermitMock.sol diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol deleted file mode 100644 index 37a860ef4dc..00000000000 --- a/contracts/mocks/ERC721PermitMock.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../token/ERC721/extensions/draft-ERC721Permit.sol"; - -contract ERC721PermitMock is ERC721Permit { - constructor( - string memory name, - string memory symbol, - address initialAccount, - uint256 tokenId - ) payable ERC721(name, symbol) ERC721Permit(name) { - _mint(initialAccount, tokenId); - } - - function getChainId() external view returns (uint256) { - return block.chainid; - } -} From e341b8302586a75c80d0424d5ab33713658766e7 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 18:57:08 -0400 Subject: [PATCH 129/300] Governance adocs update --- contracts/mocks/UserTOkenerc721Mock.sol | 32 +++++++++++++++++++++++++ docs/modules/ROOT/pages/governance.adoc | 4 ++++ 2 files changed, 36 insertions(+) create mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol new file mode 100644 index 00000000000..b3874e66da5 --- /dev/null +++ b/contracts/mocks/UserTOkenerc721Mock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; + +contract MyToken is ERC721, EIP712, ERC721Votes { + constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._burn(account, amount); + } +} \ No newline at end of file diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 6014f778dc1..13ce8e9a817 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -14,6 +14,10 @@ This governance protocol is generally implemented in a special-purpose contract OpenZeppelin’s Governor system was designed with a concern for compatibility with existing systems that were based on Compound’s GovernorAlpha and GovernorBravo. Because of this, you will find that many modules are presented in two variants, one of which is built for compatibility with those systems. +=== ERC721Votes + +The ERC721 extension to keep track of votes and vote delegation is one such case. + === ERC20Votes & ERC20VotesComp The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. From 130664e71aa56345c3ae364453404d8fc48525ab Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 14:13:44 -0400 Subject: [PATCH 130/300] Removing test contract --- contracts/mocks/UserTOkenerc721Mock.sol | 32 ------------------------- 1 file changed, 32 deletions(-) delete mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol deleted file mode 100644 index b3874e66da5..00000000000 --- a/contracts/mocks/UserTOkenerc721Mock.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "@openzeppelin/contracts/token/ERC721.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; - -contract MyToken is ERC721, EIP712, ERC721Votes { - constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._afterTokenTransfer(from, to, amount); - } - - function _mint(address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._mint(to, amount); - } - - function _burn(address account, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._burn(account, amount); - } -} \ No newline at end of file From 5c9e99396a0000653871c6318e47a31f5089f4aa Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sat, 30 Oct 2021 20:49:29 -0400 Subject: [PATCH 131/300] Initial contracts creation --- .../extensions/GovernorVotesERC721.sol | 9 +- .../token/ERC721/extensions/ERC721Votes.sol | 258 ++++++++++++++++++ .../ERC721/extensions/draft-ERC721Permit.sol | 87 ++++++ .../ERC721/extensions/draft-IERC721Permit.sol | 60 ++++ 4 files changed, 411 insertions(+), 3 deletions(-) create mode 100644 contracts/token/ERC721/extensions/ERC721Votes.sol create mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol create mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 2e3079fc243..29050b1d121 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -4,23 +4,26 @@ pragma solidity ^0.8.0; import "../Governor.sol"; -import "../../token/ERC721/extensions/ERC721Votes.sol"; +import "../../token/ERC721/extensions/draft-ERC721Votes.sol"; import "../../utils/math/Math.sol"; /** * @dev Extension of {Governor} for voting weight extraction from an {ERC721Votes} token. * - * _Available since v4.3._ + * _Available since v4.5._ */ abstract contract GovernorVotesERC721 is Governor { ERC721Votes public immutable token; + /** + * @dev Need the ERC721Votes address to be initialized + */ constructor(ERC721Votes tokenAddress) { token = tokenAddress; } /** - * Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). + * @dev Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). */ function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { return token.getPastVotes(account, blockNumber); diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol new file mode 100644 index 00000000000..20da35f32a6 --- /dev/null +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/ERC721Votes.sol) + +pragma solidity ^0.8.0; + +import "./draft-ERC721Permit.sol"; +import "../../../utils/math/Math.sol"; +import "../../../utils/math/SafeCast.sol"; +import "../../../utils/cryptography/ECDSA.sol"; + +/** + * @dev Extension of ERC721 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 {ERC721VotesComp} 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 {getVotes} and {getPastVotes}. + * + * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it + * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. + * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this + * will significantly increase the base gas cost of transfers. + * + * _Available since v4.2._ + */ +abstract contract ERC721Votes is ERC721Permit { + struct Checkpoint { + uint32 fromBlock; + uint224 votes; + } + + bytes32 private constant _DELEGATION_TYPEHASH = + keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + + mapping(address => address) private _delegates; + mapping(address => Checkpoint[]) private _checkpoints; + Checkpoint[] private _totalSupplyCheckpoints; + + /** + * @dev Emitted when an account changes their delegate. + */ + event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); + + /** + * @dev Emitted when a token transfer or delegate change results in changes to an account's voting power. + */ + event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); + + /** + * @dev Get the `pos`-th checkpoint for `account`. + */ + function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoint memory) { + return _checkpoints[account][pos]; + } + + /** + * @dev Get number of checkpoints for `account`. + */ + function numCheckpoints(address account) public view virtual returns (uint32) { + return SafeCast.toUint32(_checkpoints[account].length); + } + + /** + * @dev Get the address `account` is currently delegating to. + */ + function delegates(address account) public view virtual returns (address) { + return _delegates[account]; + } + + /** + * @dev Gets the current votes balance for `account` + */ + function getVotes(address account) public view returns (uint256) { + uint256 pos = _checkpoints[account].length; + return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes; + } + + /** + * @dev Retrieve the number of votes for `account` at the end of `blockNumber`. + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) { + require(blockNumber < block.number, "ERC721Votes: block not yet mined"); + return _checkpointsLookup(_checkpoints[account], blockNumber); + } + + /** + * @dev Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances. + * It is but NOT the sum of all the delegated votes! + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastTotalSupply(uint256 blockNumber) public view returns (uint256) { + require(blockNumber < block.number, "ERC721Votes: block not yet mined"); + return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber); + } + + /** + * @dev Lookup a value in a list of (sorted) checkpoints. + */ + function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) private view returns (uint256) { + // We run a binary search to look for the earliest checkpoint taken after `blockNumber`. + // + // During the loop, the index of the wanted checkpoint remains in the range [low-1, high). + // With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant. + // - If the middle checkpoint is after `blockNumber`, we look in [low, mid) + // - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high) + // Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not + // out of bounds (in which case we're looking too far in the past and the result is 0). + // Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is + // past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out + // the same. + uint256 high = ckpts.length; + uint256 low = 0; + while (low < high) { + uint256 mid = Math.average(low, high); + if (ckpts[mid].fromBlock > blockNumber) { + high = mid; + } else { + low = mid + 1; + } + } + + return high == 0 ? 0 : ckpts[high - 1].votes; + } + + /** + * @dev Delegate votes from the sender to `delegatee`. + */ + function delegate(address delegatee) public virtual { + _delegate(_msgSender(), delegatee); + } + + /** + * @dev Delegates votes from signer to `delegatee` + */ + function delegateBySig( + address delegatee, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + require(block.timestamp <= expiry, "ERC721Votes: signature expired"); + address signer = ECDSA.recover( + _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), + v, + r, + s + ); + require(nonce == _useNonce(signer), "ERC721Votes: invalid nonce"); + _delegate(signer, delegatee); + } + + /** + * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). + */ + function _maxSupply() internal view virtual returns (uint224) { + return type(uint224).max; + } + + /** + * @dev Snapshots the totalSupply after it has been increased. + */ + function _mint(address account, uint256 amount) internal virtual override { + super._mint(account, amount);//TODO: update for NFT + require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + + _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); + } + + /** + * @dev Snapshots the totalSupply after it has been decreased. + */ + function _burn(uint256 tokenId) internal virtual override { + super._burn(tokenId); + + _writeCheckpoint(_totalSupplyCheckpoints, _subtract, tokenId); + } + + /** + * @dev Move voting power when tokens are transferred. + * + * Emits a {DelegateVotesChanged} event. + */ + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual { + _moveVotingPower(delegates(from), delegates(to), amount);//TODO: Update to be NFT logic + } + + /** + * @dev Change delegation for `delegator` to `delegatee`. + * + * Emits events {DelegateChanged} and {DelegateVotesChanged}. + */ + function _delegate(address delegator, address delegatee) internal virtual { + address currentDelegate = delegates(delegator); + uint256 delegatorBalance = balanceOf(delegator); + _delegates[delegator] = delegatee; + + emit DelegateChanged(delegator, currentDelegate, delegatee); + + _moveVotingPower(currentDelegate, delegatee, delegatorBalance); + } + + function _moveVotingPower( + address src, + address dst, + uint256 amount + ) private { + if (src != dst && amount > 0) { + if (src != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[src], _subtract, amount); + emit DelegateVotesChanged(src, oldWeight, newWeight); + } + + if (dst != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[dst], _add, amount); + emit DelegateVotesChanged(dst, oldWeight, newWeight); + } + } + } + + function _writeCheckpoint(//TODO: update for NFT + Checkpoint[] storage ckpts, + function(uint256, uint256) view returns (uint256) op, + uint256 delta + ) private returns (uint256 oldWeight, uint256 newWeight) { + uint256 pos = ckpts.length; + oldWeight = pos == 0 ? 0 : ckpts[pos - 1].votes; + newWeight = op(oldWeight, delta); + + if (pos > 0 && ckpts[pos - 1].fromBlock == block.number) { + ckpts[pos - 1].votes = SafeCast.toUint224(newWeight); + } else { + ckpts.push(Checkpoint({fromBlock: SafeCast.toUint32(block.number), votes: SafeCast.toUint224(newWeight)})); + } + } + + function _add(uint256 a, uint256 b) private pure returns (uint256) { + return a + b; + } + + function _subtract(uint256 a, uint256 b) private pure returns (uint256) { + return a - b; + } +} diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol new file mode 100644 index 00000000000..c00d15367ab --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) + +pragma solidity ^0.8.0; + +import "./draft-IERC721Permit.sol"; +import "./ERC721Enumerable.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; +import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/Counters.sol"; + +/** + * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * _Available since v3.4._ + */ +abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { + using Counters for Counters.Counter; + + mapping(address => Counters.Counter) private _nonces; + + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC721 token name. + */ + constructor(string memory name) EIP712(name, "1") {} + + /** + * @dev See {IERC721Permit-permit}. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); + + bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + + bytes32 hash = _hashTypedDataV4(structHash); + + address signer = ECDSA.recover(hash, v, r, s); + require(signer == owner, "ERC721Permit: invalid signature"); + + _approve(spender, value); + } + + /** + * @dev See {IERC721Permit-nonces}. + */ + function nonces(address owner) public view virtual override returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } +} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol new file mode 100644 index 00000000000..61882f2de0d --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + */ +interface IERC721Permit { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC721-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} From 1ad617a5c53d7f41c7691110543c61b50086b86b Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 09:28:35 -0400 Subject: [PATCH 132/300] creating permit tests --- contracts/mocks/ERC721PermitMock.sol | 20 +++ .../extensions/draft-ERC721Permit.test.js | 117 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 contracts/mocks/ERC721PermitMock.sol create mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol new file mode 100644 index 00000000000..37a860ef4dc --- /dev/null +++ b/contracts/mocks/ERC721PermitMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC721/extensions/draft-ERC721Permit.sol"; + +contract ERC721PermitMock is ERC721Permit { + constructor( + string memory name, + string memory symbol, + address initialAccount, + uint256 tokenId + ) payable ERC721(name, symbol) ERC721Permit(name) { + _mint(initialAccount, tokenId); + } + + function getChainId() external view returns (uint256) { + return block.chainid; + } +} diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js new file mode 100644 index 00000000000..7a7b8cf5d83 --- /dev/null +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -0,0 +1,117 @@ +/* eslint-disable */ + +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const ERC721PermitMock = artifacts.require('ERC721PermitMock'); + +const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); + +const Permit = [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, +]; + +contract('ERC721Permit', function (accounts) { + const [ initialHolder, spender, recipient, other ] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; + + const initialTokenId = new BN('100'); + + beforeEach(async function () { + this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); + + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = await this.token.getChainId(); + }); + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect( + await this.token.DOMAIN_SEPARATOR(), + ).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address), + ); + }); + + describe.only('permit', function () { + const wallet = Wallet.generate(); + + const owner = wallet.getAddressString(); + const value = initialTokenId; + const nonce = 0; + const maxDeadline = MAX_UINT256; + + const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ + primaryType: 'Permit', + types: { EIP712Domain, Permit }, + domain: { name, version, chainId, verifyingContract }, + message: { owner, spender, value, nonce, deadline }, + }); + + it('accepts owner signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); + expect(await this.token.getApproved(value)).to.be.equal(spender); + }); + + it('rejects reused signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects other signature', async function () { + const otherWallet = Wallet.generate(); + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects expired permit', async function () { + const deadline = (await time.latest()) - time.duration.weeks(1); + + const data = buildData(this.chainId, this.token.address, deadline); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, deadline, v, r, s), + 'ERC721Permit: expired deadline', + ); + }); + }); +}); From 13d3b7c2fbc8a55ce123ca24d127d98ad416b533 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 10:26:22 -0400 Subject: [PATCH 133/300] Fixing checkpoints count --- contracts/token/ERC721/extensions/ERC721Votes.sol | 11 +++++------ .../ERC721/extensions/draft-ERC721Permit.test.js | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 20da35f32a6..b184c9c83fa 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -7,7 +7,6 @@ import "./draft-ERC721Permit.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; - /** * @dev Extension of ERC721 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. @@ -170,11 +169,11 @@ abstract contract ERC721Votes is ERC721Permit { /** * @dev Snapshots the totalSupply after it has been increased. */ - function _mint(address account, uint256 amount) internal virtual override { - super._mint(account, amount);//TODO: update for NFT + function _mint(address account, uint256 tokenId) internal virtual override { + super._mint(account, tokenId); require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } /** @@ -183,7 +182,7 @@ abstract contract ERC721Votes is ERC721Permit { function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); - _writeCheckpoint(_totalSupplyCheckpoints, _subtract, tokenId); + _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); } /** @@ -232,7 +231,7 @@ abstract contract ERC721Votes is ERC721Permit { } } - function _writeCheckpoint(//TODO: update for NFT + function _writeCheckpoint( Checkpoint[] storage ckpts, function(uint256, uint256) view returns (uint256) op, uint256 delta diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js index 7a7b8cf5d83..f22c904c724 100644 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -50,7 +50,7 @@ contract('ERC721Permit', function (accounts) { ); }); - describe.only('permit', function () { + describe('permit', function () { const wallet = Wallet.generate(); const owner = wallet.getAddressString(); From f9fa57aacc22a3dbc93971ae36fcba64c857055e Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:20:14 -0400 Subject: [PATCH 134/300] Updating ERC721Vote tests --- .../token/ERC721/extensions/ERC721Votes.sol | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index b184c9c83fa..146ff1dd213 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -192,10 +192,9 @@ abstract contract ERC721Votes is ERC721Permit { */ function _afterTokenTransfer( address from, - address to, - uint256 amount + address to ) internal virtual { - _moveVotingPower(delegates(from), delegates(to), amount);//TODO: Update to be NFT logic + _moveVotingPower(delegates(from), delegates(to), 1); } /** @@ -254,4 +253,17 @@ abstract contract ERC721Votes is ERC721Permit { function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } + + /** + * @dev Moves token from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 tokenId) external returns (bool){ + _transfer(_msgSender(), recipient, tokenId); + _afterTokenTransfer(_msgSender(), recipient); + return true; + } } From af0d1a54eb156aeb3076c1646f99ee13f117f45d Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:55:45 -0400 Subject: [PATCH 135/300] Updating ERC721Vote contract and tests --- contracts/token/ERC721/extensions/ERC721Votes.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 146ff1dd213..978bcd0787e 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -11,8 +11,6 @@ import "../../../utils/cryptography/ECDSA.sol"; * @dev Extension of ERC721 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 {ERC721VotesComp} 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 {getVotes} and {getPastVotes}. From 3ccfdd98d47e9f2366ef12d56f06a265037383a1 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 16:24:20 -0400 Subject: [PATCH 136/300] Adding _afterTokenTransfer to base ERC271 contract --- .../extensions/GovernorVotesERC721.sol | 4 + .../token/ERC721/extensions/ERC721Votes.sol | 19 +-- .../extensions/draft-ERC721Permit.test.js | 117 ------------------ 3 files changed, 7 insertions(+), 133 deletions(-) delete mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 29050b1d121..7156f163ece 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -12,7 +12,11 @@ import "../../utils/math/Math.sol"; * * _Available since v4.5._ */ +<<<<<<< HEAD abstract contract GovernorVotesERC721 is Governor { +======= +abstract contract ERC721GovernorVotesERC721 is Governor { +>>>>>>> Adding _afterTokenTransfer to base ERC271 contract ERC721Votes public immutable token; /** diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 978bcd0787e..18164dc6721 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; -import "./draft-ERC721Permit.sol"; +import "../ERC721.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; @@ -22,7 +22,7 @@ import "../../../utils/cryptography/ECDSA.sol"; * * _Available since v4.2._ */ -abstract contract ERC721Votes is ERC721Permit { +abstract contract ERC721Votes is ERC721 { struct Checkpoint { uint32 fromBlock; uint224 votes; @@ -191,7 +191,7 @@ abstract contract ERC721Votes is ERC721Permit { function _afterTokenTransfer( address from, address to - ) internal virtual { + ) internal virtual override{ _moveVotingPower(delegates(from), delegates(to), 1); } @@ -251,17 +251,4 @@ abstract contract ERC721Votes is ERC721Permit { function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } - - /** - * @dev Moves token from the caller's account to `recipient`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 tokenId) external returns (bool){ - _transfer(_msgSender(), recipient, tokenId); - _afterTokenTransfer(_msgSender(), recipient); - return true; - } } diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js deleted file mode 100644 index f22c904c724..00000000000 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable */ - -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); -const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; - -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; - -const ERC721PermitMock = artifacts.require('ERC721PermitMock'); - -const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); - -const Permit = [ - { name: 'owner', type: 'address' }, - { name: 'spender', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint256' }, -]; - -contract('ERC721Permit', function (accounts) { - const [ initialHolder, spender, recipient, other ] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - - const initialTokenId = new BN('100'); - - beforeEach(async function () { - this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); - - // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id - // from within the EVM as from the JSON RPC interface. - // See https://github.com/trufflesuite/ganache-core/issues/515 - this.chainId = await this.token.getChainId(); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect( - await this.token.DOMAIN_SEPARATOR(), - ).to.equal( - await domainSeparator(name, version, this.chainId, this.token.address), - ); - }); - - describe('permit', function () { - const wallet = Wallet.generate(); - - const owner = wallet.getAddressString(); - const value = initialTokenId; - const nonce = 0; - const maxDeadline = MAX_UINT256; - - const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ - primaryType: 'Permit', - types: { EIP712Domain, Permit }, - domain: { name, version, chainId, verifyingContract }, - message: { owner, spender, value, nonce, deadline }, - }); - - it('accepts owner signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); - expect(await this.token.getApproved(value)).to.be.equal(spender); - }); - - it('rejects reused signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects other signature', async function () { - const otherWallet = Wallet.generate(); - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects expired permit', async function () { - const deadline = (await time.latest()) - time.duration.weeks(1); - - const data = buildData(this.chainId, this.token.address, deadline); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, deadline, v, r, s), - 'ERC721Permit: expired deadline', - ); - }); - }); -}); From 05e8cee7dcc630ec8bff34a1a1fb33545f949543 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 16:30:31 -0400 Subject: [PATCH 137/300] Renaming supplied tokenId on tests --- .../ERC721/extensions/draft-ERC721Permit.sol | 87 ------------------- .../ERC721/extensions/draft-IERC721Permit.sol | 60 ------------- 2 files changed, 147 deletions(-) delete mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol delete mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol deleted file mode 100644 index c00d15367ab..00000000000 --- a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) - -pragma solidity ^0.8.0; - -import "./draft-IERC721Permit.sol"; -import "./ERC721Enumerable.sol"; -import "../../../utils/cryptography/draft-EIP712.sol"; -import "../../../utils/cryptography/ECDSA.sol"; -import "../../../utils/Counters.sol"; - -/** - * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - * - * _Available since v3.4._ - */ -abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { - using Counters for Counters.Counter; - - mapping(address => Counters.Counter) private _nonces; - - // solhint-disable-next-line var-name-mixedcase - bytes32 private immutable _PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - - /** - * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. - * - * It's a good idea to use the same `name` that is defined as the ERC721 token name. - */ - constructor(string memory name) EIP712(name, "1") {} - - /** - * @dev See {IERC721Permit-permit}. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual override { - require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); - - bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); - - bytes32 hash = _hashTypedDataV4(structHash); - - address signer = ECDSA.recover(hash, v, r, s); - require(signer == owner, "ERC721Permit: invalid signature"); - - _approve(spender, value); - } - - /** - * @dev See {IERC721Permit-nonces}. - */ - function nonces(address owner) public view virtual override returns (uint256) { - return _nonces[owner].current(); - } - - /** - * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view override returns (bytes32) { - return _domainSeparatorV4(); - } - - /** - * @dev "Consume a nonce": return the current value and increment. - * - * _Available since v4.1._ - */ - function _useNonce(address owner) internal virtual returns (uint256 current) { - Counters.Counter storage nonce = _nonces[owner]; - current = nonce.current(); - nonce.increment(); - } -} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol deleted file mode 100644 index 61882f2de0d..00000000000 --- a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - */ -interface IERC721Permit { - /** - * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, - * given ``owner``'s signed approval. - * - * IMPORTANT: The same issues {IERC721-approve} has related to transaction - * ordering also apply here. - * - * Emits an {Approval} event. - * - * Requirements: - * - * - `spender` cannot be the zero address. - * - `deadline` must be a timestamp in the future. - * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` - * over the EIP712-formatted function arguments. - * - the signature must use ``owner``'s current nonce (see {nonces}). - * - * For more information on the signature format, see the - * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP - * section]. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - /** - * @dev Returns the current nonce for `owner`. This value must be - * included whenever a signature is generated for {permit}. - * - * Every successful call to {permit} increases ``owner``'s nonce by one. This - * prevents a signature from being used multiple times. - */ - function nonces(address owner) external view returns (uint256); - - /** - * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view returns (bytes32); -} From b9c8e7b419887b18a7410dc65fa98adf567fa897 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 17:45:23 -0400 Subject: [PATCH 138/300] Updating tests based on new contract changes --- contracts/mocks/ERC721PermitMock.sol | 20 -------- .../token/ERC721/extensions/ERC721Votes.sol | 50 +++++++++++++++++-- 2 files changed, 45 insertions(+), 25 deletions(-) delete mode 100644 contracts/mocks/ERC721PermitMock.sol diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol deleted file mode 100644 index 37a860ef4dc..00000000000 --- a/contracts/mocks/ERC721PermitMock.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../token/ERC721/extensions/draft-ERC721Permit.sol"; - -contract ERC721PermitMock is ERC721Permit { - constructor( - string memory name, - string memory symbol, - address initialAccount, - uint256 tokenId - ) payable ERC721(name, symbol) ERC721Permit(name) { - _mint(initialAccount, tokenId); - } - - function getChainId() external view returns (uint256) { - return block.chainid; - } -} diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 18164dc6721..00a5e8707c0 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -4,9 +4,11 @@ pragma solidity ^0.8.0; import "../ERC721.sol"; +import "../../../utils/Counters.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; /** * @dev Extension of ERC721 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. @@ -22,19 +24,29 @@ import "../../../utils/cryptography/ECDSA.sol"; * * _Available since v4.2._ */ -abstract contract ERC721Votes is ERC721 { +abstract contract ERC721Votes is ERC721, EIP712 { + using Counters for Counters.Counter; + struct Checkpoint { uint32 fromBlock; uint224 votes; } - + uint256 _totalSupply; bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); mapping(address => address) private _delegates; + mapping(address => Counters.Counter) private _nonces; mapping(address => Checkpoint[]) private _checkpoints; Checkpoint[] private _totalSupplyCheckpoints; + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC721 token name. + + constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ + /** * @dev Emitted when an account changes their delegate. */ @@ -169,7 +181,8 @@ abstract contract ERC721Votes is ERC721 { */ function _mint(address account, uint256 tokenId) internal virtual override { super._mint(account, tokenId); - require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + _totalSupply += 1; + require(_totalSupply <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } @@ -179,7 +192,7 @@ abstract contract ERC721Votes is ERC721 { */ function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); - + _totalSupply -= 1; _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); } @@ -190,7 +203,8 @@ abstract contract ERC721Votes is ERC721 { */ function _afterTokenTransfer( address from, - address to + address to, + uint256 tokenId ) internal virtual override{ _moveVotingPower(delegates(from), delegates(to), 1); } @@ -244,6 +258,32 @@ abstract contract ERC721Votes is ERC721 { } } + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } + + /** + * @dev Returns an address nonce. + */ + function nonces(address owner) public view virtual returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev Returns DOMAIN_SEPARATOR. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparatorV4(); + } + function _add(uint256 a, uint256 b) private pure returns (uint256) { return a + b; } From 068da6026a4799a11c0650c844d72165283b4932 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 20:15:54 -0400 Subject: [PATCH 139/300] Updating execution order inside of mint --- contracts/token/ERC721/extensions/ERC721Votes.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 00a5e8707c0..7871282643b 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -180,10 +180,11 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been increased. */ function _mint(address account, uint256 tokenId) internal virtual override { + require(_totalSupply+1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + super._mint(account, tokenId); _totalSupply += 1; - require(_totalSupply <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } From e350d1540d09a48cd964b95a0c9b8c7f40221d2b Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 16:25:14 -0400 Subject: [PATCH 140/300] Adding Mocks for testing and integrating nft minting to current workflow test --- .../extensions/GovernorVotesERC721.sol | 4 - test/governance/GovernorWorkflow.behavior.js | 8 +- .../extensions/GovernorERC721.test.js | 632 +++++++++++++++--- 3 files changed, 541 insertions(+), 103 deletions(-) diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 7156f163ece..29050b1d121 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -12,11 +12,7 @@ import "../../utils/math/Math.sol"; * * _Available since v4.5._ */ -<<<<<<< HEAD abstract contract GovernorVotesERC721 is Governor { -======= -abstract contract ERC721GovernorVotesERC721 is Governor { ->>>>>>> Adding _afterTokenTransfer to base ERC271 contract ERC721Votes public immutable token; /** diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js index ae178d7c9de..1fb4823389a 100644 --- a/test/governance/GovernorWorkflow.behavior.js +++ b/test/governance/GovernorWorkflow.behavior.js @@ -31,9 +31,11 @@ function runGovernorWorkflow () { for (const voter of this.settings.voters) { if (voter.weight) { await this.token.transfer(voter.voter, voter.weight, { from: this.settings.tokenHolder }); - } else if (voter.nftWeight) { - await this.token.transferFrom(this.settings.tokenHolder, voter.voter, voter.nftWeight, - { from: this.settings.tokenHolder }); + } else if (voter.nfts) { + for (const nft of voter.nfts) { + await this.token.transferFrom(this.settings.tokenHolder, voter.voter, nft, + { from: this.settings.tokenHolder }); + } } } } diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 11a0f382b28..0ef80342a7d 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -1,118 +1,558 @@ -const { expectEvent } = require('@openzeppelin/test-helpers'); -const { BN } = require('bn.js'); -const Enums = require('../../helpers/enums'); - -const { - runGovernorWorkflow, -} = require('./../GovernorWorkflow.behavior'); - -const Token = artifacts.require('ERC721VotesMock'); -const Governor = artifacts.require('GovernorERC721Mock'); -const CallReceiver = artifacts.require('CallReceiverMock'); - -contract('GovernorERC721Mock', function (accounts) { - const [ owner, voter1, voter2, voter3, voter4 ] = accounts; - - const name = 'OZ-Governor'; - const tokenName = 'MockNFToken'; - const tokenSymbol = 'MTKN'; - const NFT0 = web3.utils.toWei('100'); - const NFT1 = web3.utils.toWei('10'); - const NFT2 = web3.utils.toWei('20'); - const NFT3 = web3.utils.toWei('30'); - const NFT4 = web3.utils.toWei('40'); - - // Must be the same as in contract - const ProposalState = { - Pending: new BN('0'), - Active: new BN('1'), - Canceled: new BN('2'), - Defeated: new BN('3'), - Succeeded: new BN('4'), - Queued: new BN('5'), - Expired: new BN('6'), - Executed: new BN('7'), - }; +/* eslint-disable */ + +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const { promisify } = require('util'); +const queue = promisify(setImmediate); + +const ERC721VotesMock = artifacts.require('ERC721VotesMock'); + +const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); + +const Delegation = [ + { name: 'delegatee', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'expiry', type: 'uint256' }, +]; + +async function countPendingTransactions() { + return parseInt( + await network.provider.send('eth_getBlockTransactionCountByNumber', ['pending']) + ); +} + +async function batchInBlock (txs) { + try { + // disable auto-mining + await network.provider.send('evm_setAutomine', [false]); + // send all transactions + const promises = txs.map(fn => fn()); + // wait for node to have all pending transactions + while (txs.length > await countPendingTransactions()) { + await queue(); + } + // mine one block + await network.provider.send('evm_mine'); + // fetch receipts + const receipts = await Promise.all(promises); + // Sanity check, all tx should be in the same block + const minedBlocks = new Set(receipts.map(({ receipt }) => receipt.blockNumber)); + expect(minedBlocks.size).to.equal(1); + + return receipts; + } finally { + // enable auto-mining + await network.provider.send('evm_setAutomine', [true]); + } +} + +contract('ERC721Votes', function (accounts) { + const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; + const NFT0 = new BN('10000000000000000000000000'); + const NFT1 = new BN('10'); + const NFT2 = new BN('20'); + const NFT3 = new BN('30'); + const NFT4 = new BN('40'); + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; beforeEach(async function () { - this.owner = owner; - this.token = await Token.new(tokenName, tokenSymbol); - this.mock = await Governor.new(name, this.token.address); - this.receiver = await CallReceiver.new(); - await this.token.mint(owner, NFT0); - await this.token.mint(owner, NFT1); - await this.token.mint(owner, NFT2); - await this.token.mint(owner, NFT3); - await this.token.mint(owner, NFT4); - - await this.token.delegate(voter1, { from: voter1 }); - await this.token.delegate(voter2, { from: voter2 }); - await this.token.delegate(voter3, { from: voter3 }); - await this.token.delegate(voter4, { from: voter4 }); + this.token = await ERC721VotesMock.new(name, symbol); + + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = await this.token.getChainId(); + }); + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect( + await this.token.DOMAIN_SEPARATOR(), + ).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address), + ); + }); + + it('minting restriction', async function () { + const lastTokenId = new BN('2').pow(new BN('224')); + this.token.mint(holder, NFT1); + this.token.mint(holder, NFT2); + this.token.mint(holder, NFT3); + this.token.mint(holder, NFT0); + this.token.mint(holder, NFT4); + + await expectRevert( + this.token.mint(holder, lastTokenId), + 'ERC721Votes: total supply risks overflowing votes', + ); }); - 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('4'); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16'); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + describe('set delegation', function () { + describe('call', function () { + it('delegation with tokens', async function () { + await this.token.mint(holder, NFT0); + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: '0', + newBalance: '1', + }); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + + expect(await this.token.getVotes(holder)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); + }); + + it('delegation without tokens', async function () { + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + }); + }); + + describe('with signature', function () { + const delegator = Wallet.generate(); + const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); + const nonce = 0; + + const buildData = (chainId, verifyingContract, message) => ({ data: { + primaryType: 'Delegation', + types: { EIP712Domain, Delegation }, + domain: { name, version, chainId, verifyingContract }, + message, + }}); + + beforeEach(async function () { + await this.token.mint(delegatorAddress, NFT0); + }); + + it('accept signed delegation', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + expectEvent(receipt, 'DelegateChanged', { + delegator: delegatorAddress, + fromDelegate: ZERO_ADDRESS, + toDelegate: delegatorAddress, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: delegatorAddress, + previousBalance: '0', + newBalance: '1', + }); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); + + expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1'); + }); + + it('rejects reused signature', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), + 'ERC721Votes: invalid nonce', + ); + }); + + it('rejects bad delegatee', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + const { logs } = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); + const { args } = logs.find(({ event }) => event == 'DelegateChanged'); + expect(args.delegator).to.not.be.equal(delegatorAddress); + expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); + expect(args.toDelegate).to.be.equal(holderDelegatee); + }); + + it('rejects bad nonce', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), + 'ERC721Votes: invalid nonce', + ); + }); + + it('rejects expired permit', async function () { + const expiry = (await time.latest()) - time.duration.weeks(1); + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry, + }), + )); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), + 'ERC721Votes: signature expired', + ); + }); + }); + }); + + describe('change delegation', function () { + beforeEach(async function () { + await this.token.mint(holder, NFT0); + await this.token.delegate(holder, { from: holder }); + }); + + it('call', async function () { + expect(await this.token.delegates(holder)).to.be.equal(holder); + + const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: holder, + toDelegate: holderDelegatee, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: '1', + newBalance: '0', + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holderDelegatee, + previousBalance: '0', + newBalance: '1', + }); + + expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); + + expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); + expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); + }); }); - describe('voting with ERC721 token', function () { + describe('transfers', function () { beforeEach(async function () { - this.settings = { - proposal: [ - [ this.receiver.address ], - [ web3.utils.toWei('0') ], - [ this.receiver.contract.methods.mockFunction().encodeABI() ], - '', - ], - tokenHolder: owner, - voters: [ - { voter: voter1, nfts: [NFT0], support: Enums.VoteType.For }, - { voter: voter2, nfts: [NFT1, NFT2], support: Enums.VoteType.For }, - { voter: voter3, nfts: [NFT3], support: Enums.VoteType.Against }, - { voter: voter4, nfts: [NFT4], support: Enums.VoteType.Abstain }, - ], - }; + await this.token.mint(holder, NFT0); + }); + + it('no delegation', async function () { + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); + 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.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); + + 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 = '0'; + }); + + it('receiver delegation', async function () { + await this.token.delegate(recipient, { from: recipient }); + + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); + 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.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); + 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'; }); afterEach(async function () { - expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); + expect(await this.token.getVotes(holder)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); + + // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" + const blockNumber = await time.latestBlock(); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getPastVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes); + }); + }); + + // 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, NFT0); + await this.token.mint(holder, NFT1); + await this.token.mint(holder, NFT2); + await this.token.mint(holder, NFT3); + }); + + describe('balanceOf', function () { + it('grants to initial account', async function () { + expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('4'); + }); + }); + + describe('numCheckpoints', function () { + it('returns the number of checkpoints for a delegate', async function () { - for (const vote of this.receipts.castVote.filter(Boolean)) { - const { voter } = vote.logs.find(Boolean).args; + await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const t1 = await this.token.delegate(other1, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + + const t3 = await this.token.transferFrom(recipient, other2, NFT2, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - expect(await this.mock.hasVoted(this.id, voter)).to.be.equal(true); + const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - expectEvent( - vote, - 'VoteCast', - this.settings.voters.find(({ address }) => address === voter), + expect(await this.token.checkpointAt(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); + expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); + expect(await this.token.checkpointAt(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); + expect(await this.token.checkpointAt(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); + + await time.advanceBlock(); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('1'); + }); + + it('does not add more than one checkpoint in a block', async function () { + await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); + await this.token.transferFrom(holder, recipient, NFT2, { 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.transferFrom(recipient, other2, NFT1, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(recipient, other2, NFT2, { from: recipient, gas: 200000 }), + ]); + + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + expect(await this.token.checkpointAt(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); + + const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); + }); + }); + + describe('getPastVotes', function () { + it('reverts if block number >= current block', async function () { + await expectRevert( + this.token.getPastVotes(other1, 5e10), + 'block not yet mined', ); + }); - if (voter === voter2) { - expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('2'); - } else { - expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); - } - } + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('4'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); + }); - await this.mock.proposalVotes(this.id).then(result => { - for (const [key, value] of Object.entries(Enums.VoteType)) { - expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( - Object.values(this.settings.voters).filter(({ support }) => support === value).reduce( - (acc, { nfts }) => acc.add(new BN(nfts.length)), - new BN('0'), - ), - ); - } + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); }); - expect(await this.mock.state(this.id)).to.be.bignumber.equal(ProposalState.Executed); + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const total = await this.token.balanceOf(holder); + + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.transferFrom(holder, other2, NFT1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.transferFrom(holder, other2, NFT2, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.transferFrom(other2, holder, NFT2, { from: other2 }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal(total); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(total); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); + }); }); + }); - runGovernorWorkflow(); + describe('getPastVotingPower', function () { + beforeEach(async function () { + await this.token.delegate(holder, { from: holder }); + }); + + it('reverts if block number >= current block', async function () { + await expectRevert( + this.token.getPastVotingPower(5e10), + 'ERC721Votes: block not yet mined', + ); + }); + + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastVotingPower(0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + t1 = await this.token.mint(holder, NFT0); + + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const t1 = await this.token.mint(holder, NFT0); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.mint(holder, NFT1); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.burn(NFT1); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.mint(holder, NFT2); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.burn(NFT2); + await time.advanceBlock(); + await time.advanceBlock(); + const t5 = await this.token.mint(holder, NFT3); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + }); }); }); From c5f44f79620ffa42ba57bda80cdc3bafaaa34c0e Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 17:02:56 -0400 Subject: [PATCH 141/300] Implementing override test --- .../extensions/GovernorERC721.test.js | 632 +++--------------- 1 file changed, 96 insertions(+), 536 deletions(-) diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 0ef80342a7d..11a0f382b28 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -1,558 +1,118 @@ -/* eslint-disable */ - -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); -const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; - -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; - -const { promisify } = require('util'); -const queue = promisify(setImmediate); - -const ERC721VotesMock = artifacts.require('ERC721VotesMock'); - -const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); - -const Delegation = [ - { name: 'delegatee', type: 'address' }, - { name: 'nonce', type: 'uint256' }, - { name: 'expiry', type: 'uint256' }, -]; - -async function countPendingTransactions() { - return parseInt( - await network.provider.send('eth_getBlockTransactionCountByNumber', ['pending']) - ); -} - -async function batchInBlock (txs) { - try { - // disable auto-mining - await network.provider.send('evm_setAutomine', [false]); - // send all transactions - const promises = txs.map(fn => fn()); - // wait for node to have all pending transactions - while (txs.length > await countPendingTransactions()) { - await queue(); - } - // mine one block - await network.provider.send('evm_mine'); - // fetch receipts - const receipts = await Promise.all(promises); - // Sanity check, all tx should be in the same block - const minedBlocks = new Set(receipts.map(({ receipt }) => receipt.blockNumber)); - expect(minedBlocks.size).to.equal(1); - - return receipts; - } finally { - // enable auto-mining - await network.provider.send('evm_setAutomine', [true]); - } -} - -contract('ERC721Votes', function (accounts) { - const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; - const NFT0 = new BN('10000000000000000000000000'); - const NFT1 = new BN('10'); - const NFT2 = new BN('20'); - const NFT3 = new BN('30'); - const NFT4 = new BN('40'); - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; +const { expectEvent } = require('@openzeppelin/test-helpers'); +const { BN } = require('bn.js'); +const Enums = require('../../helpers/enums'); + +const { + runGovernorWorkflow, +} = require('./../GovernorWorkflow.behavior'); + +const Token = artifacts.require('ERC721VotesMock'); +const Governor = artifacts.require('GovernorERC721Mock'); +const CallReceiver = artifacts.require('CallReceiverMock'); + +contract('GovernorERC721Mock', function (accounts) { + const [ owner, voter1, voter2, voter3, voter4 ] = accounts; + + const name = 'OZ-Governor'; + const tokenName = 'MockNFToken'; + const tokenSymbol = 'MTKN'; + const NFT0 = web3.utils.toWei('100'); + const NFT1 = web3.utils.toWei('10'); + const NFT2 = web3.utils.toWei('20'); + const NFT3 = web3.utils.toWei('30'); + const NFT4 = web3.utils.toWei('40'); + + // Must be the same as in contract + const ProposalState = { + Pending: new BN('0'), + Active: new BN('1'), + Canceled: new BN('2'), + Defeated: new BN('3'), + Succeeded: new BN('4'), + Queued: new BN('5'), + Expired: new BN('6'), + Executed: new BN('7'), + }; beforeEach(async function () { - this.token = await ERC721VotesMock.new(name, symbol); - - // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id - // from within the EVM as from the JSON RPC interface. - // See https://github.com/trufflesuite/ganache-core/issues/515 - this.chainId = await this.token.getChainId(); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect( - await this.token.DOMAIN_SEPARATOR(), - ).to.equal( - await domainSeparator(name, version, this.chainId, this.token.address), - ); - }); - - it('minting restriction', async function () { - const lastTokenId = new BN('2').pow(new BN('224')); - this.token.mint(holder, NFT1); - this.token.mint(holder, NFT2); - this.token.mint(holder, NFT3); - this.token.mint(holder, NFT0); - this.token.mint(holder, NFT4); - - await expectRevert( - this.token.mint(holder, lastTokenId), - 'ERC721Votes: total supply risks overflowing votes', - ); + this.owner = owner; + this.token = await Token.new(tokenName, tokenSymbol); + this.mock = await Governor.new(name, this.token.address); + this.receiver = await CallReceiver.new(); + await this.token.mint(owner, NFT0); + await this.token.mint(owner, NFT1); + await this.token.mint(owner, NFT2); + await this.token.mint(owner, NFT3); + await this.token.mint(owner, NFT4); + + await this.token.delegate(voter1, { from: voter1 }); + await this.token.delegate(voter2, { from: voter2 }); + await this.token.delegate(voter3, { from: voter3 }); + await this.token.delegate(voter4, { from: voter4 }); }); - describe('set delegation', function () { - describe('call', function () { - it('delegation with tokens', async function () { - await this.token.mint(holder, NFT0); - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegate(holder, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: '0', - newBalance: '1', - }); - - expect(await this.token.delegates(holder)).to.be.equal(holder); - - expect(await this.token.getVotes(holder)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); - }); - - it('delegation without tokens', async function () { - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegate(holder, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); - - expect(await this.token.delegates(holder)).to.be.equal(holder); - }); - }); - - describe('with signature', function () { - const delegator = Wallet.generate(); - const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); - const nonce = 0; - - const buildData = (chainId, verifyingContract, message) => ({ data: { - primaryType: 'Delegation', - types: { EIP712Domain, Delegation }, - domain: { name, version, chainId, verifyingContract }, - message, - }}); - - beforeEach(async function () { - await this.token.mint(delegatorAddress, NFT0); - }); - - it('accept signed delegation', async function () { - const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - )); - - expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - expectEvent(receipt, 'DelegateChanged', { - delegator: delegatorAddress, - fromDelegate: ZERO_ADDRESS, - toDelegate: delegatorAddress, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: delegatorAddress, - previousBalance: '0', - newBalance: '1', - }); - - expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); - - expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1'); - }); - - it('rejects reused signature', async function () { - const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - )); - - await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), - 'ERC721Votes: invalid nonce', - ); - }); - - it('rejects bad delegatee', async function () { - const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - )); - - const { logs } = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); - const { args } = logs.find(({ event }) => event == 'DelegateChanged'); - expect(args.delegator).to.not.be.equal(delegatorAddress); - expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); - expect(args.toDelegate).to.be.equal(holderDelegatee); - }); - - it('rejects bad nonce', async function () { - const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - )); - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), - 'ERC721Votes: invalid nonce', - ); - }); - - it('rejects expired permit', async function () { - const expiry = (await time.latest()) - time.duration.weeks(1); - const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry, - }), - )); - - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), - 'ERC721Votes: signature expired', - ); - }); - }); - }); - - describe('change delegation', function () { - beforeEach(async function () { - await this.token.mint(holder, NFT0); - await this.token.delegate(holder, { from: holder }); - }); - - it('call', async function () { - expect(await this.token.delegates(holder)).to.be.equal(holder); - - const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: holder, - toDelegate: holderDelegatee, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: '1', - newBalance: '0', - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holderDelegatee, - previousBalance: '0', - newBalance: '1', - }); - - expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); - - expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); - expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); - }); + 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('4'); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16'); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); }); - describe('transfers', function () { + describe('voting with ERC721 token', function () { beforeEach(async function () { - await this.token.mint(holder, NFT0); - }); - - it('no delegation', async function () { - const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); - 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.transferFrom(holder, recipient, NFT0, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); - - 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 = '0'; - }); - - it('receiver delegation', async function () { - await this.token.delegate(recipient, { from: recipient }); - - const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); - 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.transferFrom(holder, recipient, NFT0, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); - 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'; + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, nfts: [NFT0], support: Enums.VoteType.For }, + { voter: voter2, nfts: [NFT1, NFT2], support: Enums.VoteType.For }, + { voter: voter3, nfts: [NFT3], support: Enums.VoteType.Against }, + { voter: voter4, nfts: [NFT4], support: Enums.VoteType.Abstain }, + ], + }; }); afterEach(async function () { - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); - - // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" - const blockNumber = await time.latestBlock(); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getPastVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes); - }); - }); - - // 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, NFT0); - await this.token.mint(holder, NFT1); - await this.token.mint(holder, NFT2); - await this.token.mint(holder, NFT3); - }); - - describe('balanceOf', function () { - it('grants to initial account', async function () { - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('4'); - }); - }); - - describe('numCheckpoints', function () { - it('returns the number of checkpoints for a delegate', async function () { + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); - await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability - await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - - const t1 = await this.token.delegate(other1, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - - const t3 = await this.token.transferFrom(recipient, other2, NFT2, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); + for (const vote of this.receipts.castVote.filter(Boolean)) { + const { voter } = vote.logs.find(Boolean).args; - const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); + expect(await this.mock.hasVoted(this.id, voter)).to.be.equal(true); - expect(await this.token.checkpointAt(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); - expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); - expect(await this.token.checkpointAt(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); - expect(await this.token.checkpointAt(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); - - await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('2'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('1'); - }); - - it('does not add more than one checkpoint in a block', async function () { - await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); - await this.token.transferFrom(holder, recipient, NFT2, { 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.transferFrom(recipient, other2, NFT1, { from: recipient, gas: 200000 }), - () => this.token.transferFrom(recipient, other2, NFT2, { from: recipient, gas: 200000 }), - ]); - - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpointAt(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); - - const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); - }); - }); - - describe('getPastVotes', function () { - it('reverts if block number >= current block', async function () { - await expectRevert( - this.token.getPastVotes(other1, 5e10), - 'block not yet mined', + expectEvent( + vote, + 'VoteCast', + this.settings.voters.find(({ address }) => address === voter), ); - }); - it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0'); - }); - - it('returns the latest block if >= last checkpoint block', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('4'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); - }); + if (voter === voter2) { + expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('2'); + } else { + expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); + } + } - it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); + await this.mock.proposalVotes(this.id).then(result => { + for (const [key, value] of Object.entries(Enums.VoteType)) { + expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( + Object.values(this.settings.voters).filter(({ support }) => support === value).reduce( + (acc, { nfts }) => acc.add(new BN(nfts.length)), + new BN('0'), + ), + ); + } }); - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const total = await this.token.balanceOf(holder); - - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.transferFrom(holder, other2, NFT1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.transferFrom(holder, other2, NFT2, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.transferFrom(other2, holder, NFT2, { from: other2 }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal(total); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(total); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('3'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('2'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); - }); + expect(await this.mock.state(this.id)).to.be.bignumber.equal(ProposalState.Executed); }); - }); - describe('getPastVotingPower', function () { - beforeEach(async function () { - await this.token.delegate(holder, { from: holder }); - }); - - it('reverts if block number >= current block', async function () { - await expectRevert( - this.token.getPastVotingPower(5e10), - 'ERC721Votes: block not yet mined', - ); - }); - - it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastVotingPower(0)).to.be.bignumber.equal('0'); - }); - - it('returns the latest block if >= last checkpoint block', async function () { - t1 = await this.token.mint(holder, NFT0); - - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - }); - - it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const t1 = await this.token.mint(holder, NFT0); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.mint(holder, NFT1); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.burn(NFT1); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.mint(holder, NFT2); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.burn(NFT2); - await time.advanceBlock(); - await time.advanceBlock(); - const t5 = await this.token.mint(holder, NFT3); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotingPower(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotingPower(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotingPower(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotingPower(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotingPower(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotingPower(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotingPower(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotingPower(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - }); + runGovernorWorkflow(); }); }); From 7a9228fdef9278e5aad7a92343eee08f4d516f7f Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 18:57:08 -0400 Subject: [PATCH 142/300] Governance adocs update --- contracts/mocks/UserTOkenerc721Mock.sol | 32 +++++++++++++++++++ .../token/ERC721/extensions/ERC721Votes.sol | 3 +- 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol new file mode 100644 index 00000000000..b3874e66da5 --- /dev/null +++ b/contracts/mocks/UserTOkenerc721Mock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; + +contract MyToken is ERC721, EIP712, ERC721Votes { + constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._burn(account, amount); + } +} \ No newline at end of file diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 7871282643b..39c03934028 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -44,8 +44,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. * * It's a good idea to use the same `name` that is defined as the ERC721 token name. - - constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ + */ /** * @dev Emitted when an account changes their delegate. From 49a378f919b7b1049f5c4873ecd06d44f3d686b0 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 14:46:27 -0400 Subject: [PATCH 143/300] Updating contracts listing order --- docs/modules/ROOT/pages/governance.adoc | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 13ce8e9a817..6014f778dc1 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -14,10 +14,6 @@ This governance protocol is generally implemented in a special-purpose contract OpenZeppelin’s Governor system was designed with a concern for compatibility with existing systems that were based on Compound’s GovernorAlpha and GovernorBravo. Because of this, you will find that many modules are presented in two variants, one of which is built for compatibility with those systems. -=== ERC721Votes - -The ERC721 extension to keep track of votes and vote delegation is one such case. - === ERC20Votes & ERC20VotesComp The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. From 22c672e25e6fbe0078eb32a7812d45a26499469a Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 15:18:09 -0400 Subject: [PATCH 144/300] Delete of test contract --- contracts/mocks/UserTOkenerc721Mock.sol | 32 ------------------------- 1 file changed, 32 deletions(-) delete mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol deleted file mode 100644 index b3874e66da5..00000000000 --- a/contracts/mocks/UserTOkenerc721Mock.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "@openzeppelin/contracts/token/ERC721.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; - -contract MyToken is ERC721, EIP712, ERC721Votes { - constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._afterTokenTransfer(from, to, amount); - } - - function _mint(address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._mint(to, amount); - } - - function _burn(address account, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._burn(account, amount); - } -} \ No newline at end of file From 514e0aa9c5392e0f5fe9c3e7a4759e7efead6a1d Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 18:14:44 -0400 Subject: [PATCH 145/300] Updating comment and documentation --- contracts/governance/IGovernor.sol | 2 +- docs/modules/ROOT/pages/governance.adoc | 27 ++++++++++--------------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index 50d9d9952df..d5dc08cd661 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -193,7 +193,7 @@ abstract contract IGovernor is IERC165 { function castVote(uint256 proposalId, uint8 support) public virtual returns (uint256 balance); /** - * @dev Cast a with a reason + * @dev Cast a vote with a reason * * Emits a {VoteCast} event. */ diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 6014f778dc1..353b35f6a0e 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -129,7 +129,7 @@ If your project requires The voting power of each account in our governance setu // SPDX-License-Identifier: MIT pragma solidity ^0.8.2; -import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; contract MyToken is ERC721, EIP712, ERC721Votes { @@ -137,25 +137,20 @@ contract MyToken is ERC721, EIP712, ERC721Votes { // The functions below are overrides required by Solidity. - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._afterTokenTransfer(from, to, amount); + function _afterTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal override(ERC721, ERC721Votes) { + super._afterTokenTransfer(from, to, tokenId); } - function _mint(address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._mint(to, amount); + function _mint(address to, uint256 tokenId) internal override(ERC721, ERC721Votes) { + super._mint(to, tokenId); } - function _burn(address account, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._burn(account, amount); + function _burn(uint256 tokenId) internal override(ERC721, ERC721Votes) { + super._burn(tokenId); } } ``` From 074430d58e4a0a7edd8970878842e7c717171483 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 12:45:06 -0400 Subject: [PATCH 146/300] Update docs/modules/ROOT/pages/governance.adoc Co-authored-by: Francisco Giordano --- docs/modules/ROOT/pages/governance.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 353b35f6a0e..825bc5b2139 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -159,7 +159,7 @@ NOTE: Voting power could be determined in different ways: multiple ERC20 tokens, === Governor -Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, 3) what options people have when casting a vote and how those votes are counted, and 4)what type of token should be use to vote. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. +Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, 3) what options people have when casting a vote and how those votes are counted, and 4) what type of token should be used to vote. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. For 1) we will use the GovernorVotes module, which hooks to an ERC20Votes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. From a9d6cff2d8e3a7e3a25b1da661e5ed82df588a4e Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 12:55:58 -0400 Subject: [PATCH 147/300] Improving documentation, adding spaces, renaming variables --- contracts/governance/extensions/GovernorVotesERC721.sol | 3 +++ contracts/token/ERC721/ERC721.sol | 5 ++--- contracts/token/ERC721/README.adoc | 1 + .../{ERC721Votes.sol => draft-ERC721Votes.sol} | 9 +++++---- docs/modules/ROOT/pages/governance.adoc | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) rename contracts/token/ERC721/extensions/{ERC721Votes.sol => draft-ERC721Votes.sol} (98%) diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 29050b1d121..bfc6d86b2ff 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -14,7 +14,10 @@ import "../../utils/math/Math.sol"; */ abstract contract GovernorVotesERC721 is Governor { ERC721Votes public immutable token; +<<<<<<< HEAD +======= +>>>>>>> Improving documentation, adding spaces, renaming variables /** * @dev Need the ERC721Votes address to be initialized */ diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 6fba12a2671..31886da8cf8 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -343,7 +343,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { emit Transfer(from, to, tokenId); - _afterTokenTransfer(from, to, tokenId); + _afterTokenTransfer(from, to); } /** @@ -440,7 +440,6 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { */ function _afterTokenTransfer( address from, - address to, - uint256 tokenId + address to ) internal virtual {} } diff --git a/contracts/token/ERC721/README.adoc b/contracts/token/ERC721/README.adoc index 51089e1627c..90ec73783f0 100644 --- a/contracts/token/ERC721/README.adoc +++ b/contracts/token/ERC721/README.adoc @@ -15,6 +15,7 @@ Additionally there are multiple custom extensions, including: * designation of addresses that can pause token transfers for all users ({ERC721Pausable}). * destruction of own tokens ({ERC721Burnable}). +* support for voting and vote delegation ({ERC721Votes}) NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC721 (such as <>) and expose them as external functions in the way they prefer. On the other hand, xref:ROOT:erc721.adoc#Presets[ERC721 Presets] (such as {ERC721PresetMinterPauserAutoId}) are designed using opinionated patterns to provide developers with ready to use, deployable contracts. diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol similarity index 98% rename from contracts/token/ERC721/extensions/ERC721Votes.sol rename to contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 39c03934028..b6f851c7df7 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/ERC721Votes.sol) +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Votes.sol) pragma solidity ^0.8.0; @@ -22,7 +22,7 @@ import "../../../utils/cryptography/draft-EIP712.sol"; * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this * will significantly increase the base gas cost of transfers. * - * _Available since v4.2._ + * _Available since v4.5._ */ abstract contract ERC721Votes is ERC721, EIP712 { using Counters for Counters.Counter; @@ -203,9 +203,10 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _afterTokenTransfer( address from, - address to, - uint256 tokenId + address to ) internal virtual override{ + super._afterTokenTransfer(from, to); + _moveVotingPower(delegates(from), delegates(to), 1); } diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 825bc5b2139..b83a67f26c4 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -130,7 +130,7 @@ If your project requires The voting power of each account in our governance setu pragma solidity ^0.8.2; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/draft-ERC721Votes.sol"; contract MyToken is ERC721, EIP712, ERC721Votes { constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} From 05ed326f5ccc28025b4c629204200e997c9ed022 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 12:46:21 -0400 Subject: [PATCH 148/300] Update contracts/token/ERC721/extensions/ERC721Votes.sol Co-authored-by: Francisco Giordano --- contracts/governance/extensions/GovernorVotesERC721.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index bfc6d86b2ff..63f62b9775a 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -14,10 +14,6 @@ import "../../utils/math/Math.sol"; */ abstract contract GovernorVotesERC721 is Governor { ERC721Votes public immutable token; -<<<<<<< HEAD - -======= ->>>>>>> Improving documentation, adding spaces, renaming variables /** * @dev Need the ERC721Votes address to be initialized */ From 3eb6d7482a82560e23a9116ec83328a4d7e045a7 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 12:53:42 -0400 Subject: [PATCH 149/300] Update contracts/token/ERC721/extensions/ERC721Votes.sol Co-authored-by: Francisco Giordano --- contracts/token/ERC721/extensions/draft-ERC721Votes.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index b6f851c7df7..ded97bcfc39 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -179,7 +179,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been increased. */ function _mint(address account, uint256 tokenId) internal virtual override { - require(_totalSupply+1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + require(_totalSupply + 1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); super._mint(account, tokenId); _totalSupply += 1; From d8ff5ccf9f8e80ff1a7b84527ea51529dd89ffec Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 12:54:01 -0400 Subject: [PATCH 150/300] Update docs/modules/ROOT/pages/governance.adoc Co-authored-by: Francisco Giordano --- docs/modules/ROOT/pages/governance.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index b83a67f26c4..47beb67ce10 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -167,7 +167,7 @@ For 2) we will use GovernorVotesQuorumFraction which works together with ERC20Vo For 3) we will use GovernorCountingSimple, a module that offers 3 options to voters: For, Against, and Abstain, and where only For and Abstain votes are counted towards quorum. -For 4) we will use the GovernorVotesERC721 module, which hooks to an ERC721Votes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. +For 4) we will use the GovernorVotesERC721 module, which hooks to an ERC721Votes instance to determine the voting power of an account based on the NFTs they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. Besides these modules, Governor itself has some parameters we must set. From 367f9bbe917cedb64c5f6c9c97fa4248201f26a3 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 14:13:58 -0400 Subject: [PATCH 151/300] Adding constructor --- contracts/mocks/ERC721VotesMock.sol | 9 +++++++++ contracts/token/ERC721/extensions/draft-ERC721Votes.sol | 3 ++- test/governance/GovernorWorkflow.behavior.js | 1 + test/governance/extensions/GovernorERC721.test.js | 2 +- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index b47bfd8f24e..27a2f320ab0 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,7 +5,12 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/draft-ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { +<<<<<<< HEAD constructor(string memory name, string memory symbol) ERC721Votes(name, symbol) {} +======= + + constructor(string memory name, string memory symbol) ERC721Votes(name, symbol) { } +>>>>>>> Adding constructor function mint(address account, uint256 tokenId) public { _mint(account, tokenId); @@ -19,7 +24,11 @@ contract ERC721VotesMock is ERC721Votes { return block.chainid; } +<<<<<<< HEAD function _maxSupply() internal pure override returns (uint224) { +======= + function _maxSupply() internal pure override returns(uint224){ +>>>>>>> Adding constructor return uint224(5); } } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index ded97bcfc39..d2b49126e27 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -45,7 +45,8 @@ abstract contract ERC721Votes is ERC721, EIP712 { * * It's a good idea to use the same `name` that is defined as the ERC721 token name. */ - + constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} + /** * @dev Emitted when an account changes their delegate. */ diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js index 1fb4823389a..1075d988a6f 100644 --- a/test/governance/GovernorWorkflow.behavior.js +++ b/test/governance/GovernorWorkflow.behavior.js @@ -71,6 +71,7 @@ function runGovernorWorkflow () { if (tryGet(this.settings, 'voters')) { this.receipts.castVote = []; for (const voter of this.settings.voters) { + console.log('voting',voter); if (!voter.signature) { this.receipts.castVote.push( await getReceiptOrRevert( diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 11a0f382b28..2b4a687ee77 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -59,7 +59,7 @@ contract('GovernorERC721Mock', function (accounts) { expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); }); - describe('voting with ERC721 token', function () { + describe.only('voting with ERC721 token', function () { beforeEach(async function () { this.settings = { proposal: [ From cca8ff20bd1f92da4c1fec4954b82fa0db8bcbc5 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 14:53:24 -0400 Subject: [PATCH 152/300] Updating tests setting more NFT voting power to single voter --- test/governance/GovernorWorkflow.behavior.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js index 1075d988a6f..1fb4823389a 100644 --- a/test/governance/GovernorWorkflow.behavior.js +++ b/test/governance/GovernorWorkflow.behavior.js @@ -71,7 +71,6 @@ function runGovernorWorkflow () { if (tryGet(this.settings, 'voters')) { this.receipts.castVote = []; for (const voter of this.settings.voters) { - console.log('voting',voter); if (!voter.signature) { this.receipts.castVote.push( await getReceiptOrRevert( From 1e0f5435b4ef174f7c1820773f34665354000d26 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 13:00:30 -0400 Subject: [PATCH 153/300] Update erc721.adoc "Voting rights" are generally fungible --- docs/modules/ROOT/pages/erc721.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/erc721.adoc b/docs/modules/ROOT/pages/erc721.adoc index 14dbdc97606..8d28fad2e6e 100644 --- a/docs/modules/ROOT/pages/erc721.adoc +++ b/docs/modules/ROOT/pages/erc721.adoc @@ -1,6 +1,6 @@ = ERC721 -We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate*, *voting rights*, or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. +We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate* or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. ERC721 is a more complex standard than ERC20, with multiple optional extensions, and is split across a number of contracts. The OpenZeppelin Contracts provide flexibility regarding how these are combined, along with custom useful extensions. Check out the xref:api:token/ERC721.adoc[API Reference] to learn more about these. From 84d97db3c48b2f9980cc1802b8b9218201b23ede Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 15:35:32 -0400 Subject: [PATCH 154/300] Adding method to return token voting power --- .../extensions/GovernorVotesERC721.sol | 1 + contracts/mocks/ERC721VotesMock.sol | 8 ++++++++ contracts/token/ERC721/ERC721.sol | 14 ++++---------- .../ERC721/extensions/draft-ERC721Votes.sol | 16 +++++++++++----- docs/modules/ROOT/pages/governance.adoc | 15 +++++++-------- openzeppelin-solidity-4.3.2.tgz | Bin 0 -> 199211 bytes .../extensions/GovernorERC721.test.js | 2 +- 7 files changed, 32 insertions(+), 24 deletions(-) create mode 100644 openzeppelin-solidity-4.3.2.tgz diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 63f62b9775a..29050b1d121 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -14,6 +14,7 @@ import "../../utils/math/Math.sol"; */ abstract contract GovernorVotesERC721 is Governor { ERC721Votes public immutable token; + /** * @dev Need the ERC721Votes address to be initialized */ diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 27a2f320ab0..346c44914d3 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,12 +5,16 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/draft-ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { +<<<<<<< HEAD <<<<<<< HEAD constructor(string memory name, string memory symbol) ERC721Votes(name, symbol) {} ======= constructor(string memory name, string memory symbol) ERC721Votes(name, symbol) { } >>>>>>> Adding constructor +======= + constructor(string memory name, string memory symbol) ERC721Votes(name, symbol) {} +>>>>>>> Adding method to return token voting power function mint(address account, uint256 tokenId) public { _mint(account, tokenId); @@ -24,11 +28,15 @@ contract ERC721VotesMock is ERC721Votes { return block.chainid; } +<<<<<<< HEAD <<<<<<< HEAD function _maxSupply() internal pure override returns (uint224) { ======= function _maxSupply() internal pure override returns(uint224){ >>>>>>> Adding constructor +======= + function _maxSupply() internal pure override returns (uint224) { +>>>>>>> Adding method to return token voting power return uint224(5); } } diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 31886da8cf8..b90f83b4f3f 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -423,23 +423,17 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { address to, uint256 tokenId ) internal virtual {} - - /** + + /** * @dev Hook that is called after any transfer of tokens. This includes * minting and burning. * * Calling conditions: * - * - when `from` and `to` are both non-zero, `tokenId` of ``from``'s tokens - * has been transferred to `to`. - * - when `from` is zero, `tokenId` tokens have been minted for `to`. - * - when `to` is zero, `tokenId` of ``from``'s tokens have been burned. + * - when `from` and `to` are both non-zero. * - `from` and `to` are never both zero. * * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */ - function _afterTokenTransfer( - address from, - address to - ) internal virtual {} + function _afterTokenTransfer(address from, address to) internal virtual {} } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index d2b49126e27..2328c80d131 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -9,6 +9,7 @@ import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; import "../../../utils/cryptography/draft-EIP712.sol"; + /** * @dev Extension of ERC721 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. @@ -44,9 +45,9 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. * * It's a good idea to use the same `name` that is defined as the ERC721 token name. - */ - constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} - + */ + constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} + /** * @dev Emitted when an account changes their delegate. */ @@ -181,10 +182,10 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _mint(address account, uint256 tokenId) internal virtual override { require(_totalSupply + 1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - + super._mint(account, tokenId); _totalSupply += 1; - + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } @@ -286,6 +287,11 @@ abstract contract ERC721Votes is ERC721, EIP712 { return _domainSeparatorV4(); } + /** + * @dev Returns token voting power + */ + function _getVotingPower(uint tokenId) internal virtual returns(uint256) {} + function _add(uint256 a, uint256 b) private pure returns (uint256) { return a + b; } diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 47beb67ce10..8986ff2e069 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -132,24 +132,23 @@ pragma solidity ^0.8.2; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/draft-ERC721Votes.sol"; -contract MyToken is ERC721, EIP712, ERC721Votes { - constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} +contract MyToken is ERC721Votes { + constructor() ERC721Votes("MyToken", "MTK"){} // The functions below are overrides required by Solidity. function _afterTokenTransfer( address from, - address to, - uint256 tokenId - ) internal override(ERC721, ERC721Votes) { - super._afterTokenTransfer(from, to, tokenId); + address to + ) internal override(ERC721Votes) { + super._afterTokenTransfer(from, to); } - function _mint(address to, uint256 tokenId) internal override(ERC721, ERC721Votes) { + function _mint(address to, uint256 tokenId) internal override(ERC721Votes) { super._mint(to, tokenId); } - function _burn(uint256 tokenId) internal override(ERC721, ERC721Votes) { + function _burn(uint256 tokenId) internal override(ERC721Votes) { super._burn(tokenId); } } diff --git a/openzeppelin-solidity-4.3.2.tgz b/openzeppelin-solidity-4.3.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..927e56027f696044c87e4fb5500a59541497e9c1 GIT binary patch literal 199211 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≪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%0D88Jl0bKxb@g^9a8 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 literal 0 HcmV?d00001 diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 2b4a687ee77..11a0f382b28 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -59,7 +59,7 @@ contract('GovernorERC721Mock', function (accounts) { expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); }); - describe.only('voting with ERC721 token', function () { + describe('voting with ERC721 token', function () { beforeEach(async function () { this.settings = { proposal: [ From 8147461d76085a1c6b7307fa951b18778b3c343e Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 15:36:25 -0400 Subject: [PATCH 155/300] Delete local file --- openzeppelin-solidity-4.3.2.tgz | Bin 199211 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 openzeppelin-solidity-4.3.2.tgz diff --git a/openzeppelin-solidity-4.3.2.tgz b/openzeppelin-solidity-4.3.2.tgz deleted file mode 100644 index 927e56027f696044c87e4fb5500a59541497e9c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 199211 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≪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%0D88Jl0bKxb@g^9a8 z2rQ_Ok|EP}QVCpb<`HFHZb=8}xRw~Y^bFe*ajeC9pR(+&7;OekSfDnfA#5m7j@bY) zD=%H`PaDFclvkj{^?1QZKSoBLOj~_2Ro=P|o3Bzs?Ri8+Ny2T={g$3SkG`~kI2>** z5Y820o+_JMWV9o-Red~aj-Ai7w7*Vz{d9mY(48JDYquZt4BB(ba(_4{9nL;q5Le0` z^rCS~alVGJ(l2$B>Z7K8&n{E{BU+KDYVI%DRHdFA<{>fsP`|Rz5;G)HT`%%+&V`e! z3RYempX$9koMqY=x4N)(JG+g)913}90PuG*l;-E&o5MSr6?4I#Pw{)qArCp@L!zYC zIT@i35>UOpUIr^$`M&S>rsQh3d*9!-J{aCv7SADh#fMUQ=9%s}DtK#@Pz1nOF^@+3 zOh;R2BbL4Ls1^#n(DL22?K%lo}`OujPS=CYZb_xtbpR>mFhcuYmpT@Pptw z_Bg2jlel^9t9x^JKlpv;@MiK4InLfsl+-6gxxy@#Q>sRI} z%HC{+f6r3(E?qye2BiBa!}B z9__I9H^uhW{7YDxcU?L2Gx8wX`F;AE31t{*sP6cQow~(+`^oyj-1|B3I9@YY!JC=5 zE#Db4BX_tge0Y7Ei2wI3Zn?CQ^#_U6mcF?1=dJab)hK!N%khOaxw#dm8m$?|$j&DT z-gIphFCc7Z{3K_a-cweWV?zv-MQ5I(G_`Ty*_3bF{adrUx#T$x(~22h`Cjhg2;lKF zC;sc&w_D>=Y3SxWsHmMIpDXcQHA=hjKK}!MpPHAQr=sS7Md##7EQEF^dZo*!bEm3i z`o+w2!bI=~Pm?3G`MabILq|YP zd1^-B@n;ngEva{6gT(k&z&x)35|9LzBsv6W{vJH5<$NGsS zyAX?hoU^P4WAPvDZp8I0VDi(^=<2KKrxq*hph<@fCL}`NH>6B0{%YCOI4j}W+Vw3t z?TJ?Hi56^H>m9?G38h}>nP9mcNkcMcX~>Wnb!x#qty{n62{gjn^r5<_8KAPUMS@HI zUW4#iM{Tml_VWIQOMHJ!nwTHQ2{#&dDZafd0}1f+v2o6DTB`X5St$)Fstb;es_c~J z$c7SdcDOQzf9pQO_j9DCP4qzIA%!h_y-iUR1SClaEe$?&k9k=Q>SY)EH6-jU;X7^7 zF9?RKz5s>(41#z-=1>HObxkO*iSj`tY0&a}uailGn$}8SV)`8NMhSgFZ2D=>!%2p? z{A+g$Jrp}^2X23Z3jbIofbAhTJ{t{`#z7X(Fw#EZ75;F#4u)w8(gVr@t+#W>!#^0u!LiK9R$*98 zgrw>Vckm=^EeT=LQ%n*nATEIlB1wM+LhBYZd#pg6$d1r*L^9bDj1^kbK!nj51KWtQ z`bRDQHtz(=lLb+5lVUK812{j4xLbaI@;XHEF1MduE-ks5VX11ISjWkA-^#kF!DWCZ zXvsa4WL57IE>7g~q$eK}J8t!H+F|}1WV|WBuJ|IpbNueHLryIe{t+T9WyO0!P7Qk! zm-~T(D#r_Op>9MGre^*(&dDwK1+15aYH2x1A-S)l&mA8&jETGIJVed6BwKEVdPAPV1K?Yb2-z>h`kSp zAm^lvmQylMTZF>&bP)pTEWBKT?N^izvkTtjVIu1)zAw|d@okQm0AaYlg!d? z|5PRPLCkvt-vCFi)sX_oHJvN8QxD_s-E-2)M_T$ko3UwKxVLDoUf`vw@THy{KDVW-=)(K z2Pl&3n7yTLHW;(LUR86vVCh1GGnUUzWTUj(Sudn#I`Z-PAL_vAvG9rT;a(MZCs-$Q z!R_x|==>=-Q*vZl$^Wc}6rhi)sKvoA0QrRsta*N!xb%M>ScW_s*npE~`!5!fV1C)H zdnQRx@;6c`BafthTS(7wLl}%owM|zoKLw>h#dcYPgeUhi)>m9W6mu2nbMR(pL;?My zVM`{$rW$s+CUKXWK=1@L#FAf@)pBs3r-8gfm4FylwbiHvM&+;eh`udSD6I7$G7|9# zNnz(eS$=YP95kKsZR^7kg@H$~NVCex*JEtL?%&{- z2%L=-4*Z8oEE@BA7{T7>>p8jkq<&Qc>&5}U(D{3=L!H!4mDCO`y#o)LdR%i->?VMT z6rY{e*&}^Z0Z@dX-uZ6p&t&OowX8fR<~^u+N_*02D&XifQqNZ)Bib}d`P5z1ghBnv!)H_Ef%GaasAULgj` zY0sF4R0ZcC71V<;G>ftAki5^VRGCAlnd ze812hfYZ%Kj7jA?-@FB*jHjT4$Z|TZml&NSE1JYsM&uH7-n5SyiL7 zQ$LeAxA>(~^!2b##)p`kks|XAw+BwD57Mk4LzuH3q<0kjiuC{$8?0aQ+-MjLepSht zAgs>hR`zG63x|PuGki}~d{E(96It$w+7v{AxgdFy;sJW9VzKF~GKC7-DTbn%qd6j@ z<2Sn9g+9nVwsD`9H9b?W3Di;l$weRxz9Vdp@;rY$YxQ}vfHfV|Vq^8B2u5wT{!1lSU|;FjQituDm>Ao5rJa`wf#fM`8vmFUcip;nix7M#bW=?9*k?<33b z!OIDx4Tm-w{fC$z30ser5d#@2_&6MO)GErAV6Jei112=oKi>Pt0N4FYs+V^L9&5Bc zSNbY_s!fyo$5-!f=+LW#&jvp{;f~zjsnP9!wKgy5?5Wd93r{#2jrWTyk^WsnUPQZ7 zO#w+e#ujR5&WmCni_gUC9(A(ht^5*MIHAL234tBr^%rns`SvS1u_Ok*A*ws^hO#F? zd`oe|Tg?_zp9=0s3+~wD)$rCJi~ZxZB?Amq8QxkzDSRUsW#YZ2>zaEAu!`1Ts9G~* zVO67Vf)onwmJi0`An(QP#l(q!P?iCm^eZQhYb)n1+N?oRfU1?q} zrp`96PPP(_$e@OeEa+NkVby#oVpY82U%1X_L2p%&_d+mE(&NurUg-DQ8|%iCiixbzuQ2y+6;Q)LPO+G}gT4tO;Iz zTHOjnVl}P+`wFOT&M)#^jI3ml9QX7f4V(n!AmbJn!I<4F5|(N%A%UD06#@1!W#B-e zDZ_3t&^pO49#K>P-%%K)N8E_viF?u^i`fO`a}+`6P$)o_mw_jZ5Sx7BCeaTmq9Vdw zRcru531)Z`C(DL9fe$<+p2i!UmDk35@k_z^g}rE!M&$7XRDK{ll`qbQ;Dw+{&2?Ip z?X@n1?)W#O0>Rg!p9KVgGUL$V1U;QO*5>bq?6$VVX}sJ zH=BN}!M$xSGf&N1^|NL`?o@$uZ~fydk=r&<_C5R?K_M1Q8Te&}&ct<>Hr1U*u_G&t zUHDDgfJk>JAMGgEM~v|nj|R!j3enK*+QYTzo@^?$FELxN zUvF zHaA@c}aJ{&2%irg=Tz0re zK8H4lU`fU|9IxX-T8Mt5Iqm8JvVin14G ze&8uw@KY^-kF>t2vI74T%Hr$+sxcp2Nd@1I7X{4{s7>_kA(uayFHEoumU(R|wi|O^ zMOlFb0Dz}>!u2>HaMY$`f**`+z}@h~iduYl@m2C0>y$Pp95pFSiRNLrSuX>yZ~l%~ zM8qaEIH_%rOp2zC;y+u58KWp(FJwayjNguv5OCw8<=Wbp=3A0_cQ7EaG5!&UY8p(y zpoQy=f}KE)@^9(Bnx|r#vP^mV<`o*w?V(00CmABZ=?G#sCCjn=I`-h<_w`Xce`peP&A?=pAQ9`OB24jq#l#-5$BR^d-xzoPEK2q_XgcHe%y~p5y9G9c6kC7Ui7RjA3G@H(>C4k{wb|Q*tC^tMZG`wOxNu8 zFK1$)2L6efTn)SU%@^+R}7SC)eNR?{` zT4V~f>4}F3DWApU&Q!aF@MvHYdcqF#;>6kdy}g3_g~kZh&iZoP)w~)tonM|>!&2## z6LftcY5mY5({;UMxA_F$+w5jsr(zN)4UgOokAU>kkpcbdEOPmI&;K^N=tLx}ueQvx-IjCh?(rUJY z`wv*VjfH|3CzE+}xj{&q`O|Jt*c=eh-)}@-e|nAJh-EH|u4NviW|nQl;C_c^mKt$t~zZc?ncvh~0ehkqk3*C<~u`w1%5Ht!p(Kf^|( znSNYfOI$xKIX_LdpRf-%gEx5`KQ$#Z(QmC-4|bt#Ovmez?Iz#AHZGME@-LSg^=jE) z>+3h^LH}wd1A?|e-7}V0z)?8+AA3eb>2Qo3vc_?T5mlXRY#ge-yqITLbwM{T^M9|L z8+h1ZW0hy@y$q~YU`^ajdXB1p8PWmeC|TORuTYA)^Gbb%X%A|_NYCQxmQL0?rmNhr zDcJVu#rIbx6jgCBJks-@0gdE;SYb#;=t>GIQErI)O~!B5CP4=F*tnCva$E>X|R(X zN@ILJa)@#^6#S42fn`5~$-rv;OcyY76|9<|Ynig;vh^0_QallzUxSYp4oePQ@&;Ll zR*-0x6mPz3=qziJ%ZnqLzxkJ#--aJ|Vqf7CUtFg?XB>b{K*0!OAm9LR%6d0Idka4J zQ}Vh@qRD6Am$Cv_(@^x2De~n{TB~`QQXjdT38&A8S0tf&oORf6CmC)7PPkW%;TfRq z-07};^LW|CO?gFrkFK=LPd=RM>I3`VWtjw6T3s|*bNQ@CLA%R2k0d2tA%){iFyaW!f$G|6#GT$43TMh z|3`d|YZ*t%wY>g!!MEty=z;h@%Kx7^vBxy)yD`+<|NmmjI+5o4)8;NUa;f%4rGV8E zg+ki#>DOhjdivA;_F3uY^TgF*`|g=ES9@RRiE)iK@oLhfd&SDQZll-Z@w5KWmCI^b z>AFMbt5h%1@jA8Bda*Ri3V@Vf)7kQYs^4wH<<={-12-l6>o~~wVGX@sCMM#xziQ=b z52%T%9+_X!`Su_|J8u1W|8JgyY_Z$#A=hqi0=7;Ops?FOjQx0m;aCgbPPj+<;MR*T zQbmn9piGPT~{)z5XsV+4G#61m#EQ2)yXAtvacJtCvOkuuTg16 zP7FXPgO^uC(jZ$IS4smLdbrD8!t27ct>-Tz2aZ+xI{bj<{9oUUx2IYD7Vix{S){ez z%b8HZTAd4#g}H~0I|9I9#wb2duWcGaDl(Vyx%6`9+INQfypK$UA;*Y*m`T$uUZT&P zA5W%tawu$#O$(baH?oS;sa!2oL@SrV7>BVpQ^adWGaIomIz3pqSKk|7rp1Puv99L~wY&m0{~k|99>eo;Rx?jTH3lATpAzHHIWJxgB?*KFqMd^F2urX*ya zE6rQgYia3cl+KE2dxVRYd7{$~?RC_oUS{fO`K&!Zkjv1r4)gC-*k+lY6qRPF1AZ|1 z8H;am#!2Qg-5?dRCLsUk^GUW3#7gr!0LF1~?T1jq3_gSz+?UMoku8T3Ht$bxS@>8! z8txxYtARbDu#k`9P5mcfTiP-$cf#X_Ra5dGAMnoX=q4tX3bjk*kl3~v@;z~7Eu_ll#wtD^#`l9Dr_DSgVD+s-uwm@6&?4kKDq3dzXYl?gYLWAa zSD|e0t4HT-gH5|JLf==31U^!F6{g8-l(;M--C8%8Ey+zk?dQ!~?c1)=MsXarUZ#To z===M*XZ=Z@S*8)IaSX& z|KgmoQO^_^i3>3o2w{S)%p06F0>~f3a!Cxb?b4A!MFnNKZm!Jeh=p1q2wdxKl3SUW zbTenk$92kYnq#xNQcj!yS5_HT)S$p3r3nvzYZI; z(ny1Kyl2?I31Iffreav zp%z4gg~~~OLOf9b@8Q5kgc6(J-o`+dly{$X0`10xByg^w(=XbZ2j`b4T|Nsy3{`jv#cp!^+)ukW z4!f-g&B^RQ<~8%Ysw=}ZAKz`YQuI2VuNfcgHOzj`F=GqtYYG0CGdUmvM!|!-I2=aL zTjk=YQWDb)XZCi(t&;0R89blwRuhnA0zQzeDi1wq3U=T^el3M~3VH&>E$R2wv}7as zE325Wul7e&k}oo<(?yFv5B2Xp3~Aa%z5A$b`m%z*k=sM>nuYifidfV z*;t&j#aT2BvNm-;%qk%{UE>rY$KaXM)nkfnVC1X=u@d!_x^`~ba`OG)EN|hAjYuo~ z_T?DY3`1mU0Js=i`k+W+*;KIh!xd9~bSh}z4D+uR)OqpEssQR=a#;8ilc%xSzw=PS zGLDttZVOLs#kaO+ILZ6nR;t=eZQ63TOgce&kbcWR?f2TA^EI)suXd;n(l11;1UcGB z^pc^KhUih_1|IsPU$|gmQ$l)YT=geXWn9+FNnneXW{xCEe5LWE~M;J#LIG&(6BzSY`#dDSue z(jBDnNrDW`O;&2_OI~A^oz;Np_xPzK@5aswGM=d+hWCmv(B6DQb@*Gg#?Eq5QN;)n zsw9_*=_+hqYM2({P^B235(m!tT2O>Y!n1#^kUZ>Hvoa)+LOP_gIY`eW-5fJo*)_s? zJnP7Hf+XSoMlyB~uoCJgm1MF@m$B3;l#WRCs_ex(yfuPl0!ff4Q@|9JnwUs`0LzIK zkoPl^sPn1?9?e>uBgIJ9RXG7A&p)anQJbA%%;f5G*jY4yOlU-hB2kOkaN8*d9(7#_ zK&CVjyMF!34zFLet7-*ZzvCjN;aAnKYWsyueHv)+KN886CkSLhfb>T;OP{0qjWDz#l(UohpOLk7;+tn zx-!Fbp%`Q$N&gK7GKGnno21_nUFn-36XlaY6dwZ^u1!x6#R0{I#vi54&G zn&WJyHAl{GI(na!{|)$oAZVv}Or#q^uxu&Ox%qn1hTR zZ~f+8Q4W)QF4+=Q>ilcA(?U=pQF^WUBax=FJLAC2K)_wI;fP2H#mwycL8@gkwh5=( zyTQbOErF&~6BA6jFvpr>r3nDiPNIcc%OH+a3(kwB`L`kVU5fFjc4W>PKJ%&`;{s@^r}mMrBQc)6CzanyN@hNHT9Q>Hb?tG8F<30V#S zm*3q~(X=4ds^R71Saa;N97{P+Y%pnpoUpq2UFBx^uW1Srt?6_09BYkcVlB7|`8m(h zy|EN2$dU)+3EP({c<~l(NrY+|i%Z3ZBTV8Lxt1!L&5N-Vt>d30;|b62iT|>AH2&#E zqGdbT3;Uo){G0y7|Ed!cX(lv(_dChD-2^U1s)ahMFo6ckE61_H#O--3W$QOh@mOI_ zr1|hIo=pGUmQoU}m<6Wbh2IBinqVZ^!cD!_Q1LtWeWl;ITQA+7wHZ%1WLR^Eo~L@a zI}>X?H;t5hS0!D`wP54LTWjd3HJJ2X6RgKua|r%`YmuN5x`g>=az6@Eg?n2>jvDxL zLg@?@$rD&hM0bvwUM zwf|jpC^^~MQKAm-|6X+dFJ1WjIzNvLpDw(T%c3rLf*Vri@KFAnKpfIfprT0vl#}>LvxQCut~dG?LWW$+V0`nwkkP$FZPzF z>R%;>bIsN_Xt8{yW36l}Y<#Wl+TSCF zZg_TwCrf$1-*ula{83jpV%CcumO?MPlqVylDm_?;mS3_Bk1MacYh!1tfREDPcW=(w z%tAdi@}=8=7une{C#QgbAOQFxd_|6UoE|X$j%fo7>0vupip+M>xXq=^$|!S8s5_40 zXEV?dHH!(o&_SMoyPU;uJ5;H8B<%?qs40_$ANtnVs`F|fJulJ$-OXVp-YdE=GSD)U z-5!Xv)YtsLD@_CrplaLnslPU23{r-xNo1R?3~xoDrB2!r>f^%Uv!*>QW_37n7C1tl z>&b7J3^wJq9#CEzDaF$?-rIN7^?MK3b>tGiwBFluDT(iiUNFgd^X@TPy62VEQ|N*t zzgdBE$lAX!`*9dIu2hrW+a8EidzlUHlXllo8Wsf4N%ot(Sd5bvbXlk*i^-`BbMUZ{6uXZKf3d@lzX zp)i!fQM~HKsnO8F4@S`wC!vPEi0=^9N$YVuY+)i9UA=R(4(rLe&ErQVJZEvy3qoAk zVeWVe|AcxSPrjwKyjRnA99NpS&R%$j;dn2Zp>;DQL*L|YYgzT9Ab`qUbntxftOU#L zH$oGQ->vsIwql!#P04#lwH`6ZU#LG|$UYN2{qNZf8=G=D+J2Ee&@SR$8+3^YssZp6KPRX7^Qj$STb37D@FNXR0%XC zfwZO8T%)M*E#%h!TQdA(v}UkcS5!}AW20}ezwr1xAtyJ*;l$N^!SOJ0sv`hdc`~rH z(6H|e`@WD+xwfnD<~$x%?0CNHaR0ZdA&z=|CTq5YUX>w^8+X875O=pps2pEC1Z3oCYA9d)^=&Nma zbZ^7G9&=dQRIvgPc!Z?>qK%D=wEBuo4>5=E|`iyC>mw(j?;89(5$^`bh5e zg1xW|NW@>X)jHwwcmcYiX&1Xl+2ia7L)hd-CMvZ*RM=8}+Fu~x>`{2Agg!JKE7bia zBl2<42>;!*t&@Z{8LMSj>ZR3#chkIi z4OX4bn>l2s_w_b&=0RPTqF6WJn={){I)&}CeBJ&BibFtHtaGj$)s7{c|^U(nV($J5jORVsXBY?FD;RslyZccKBhCtr8%vbedcgvjgyxmS*` z2c!Srl;xbQIG|m{=Xw4eHTC;-1Oh>cV*e}mUzc#Sl9YnK*K`cP$=5@Z!?x|`hrutW* z;&mdkf|suI@MD1m>P{cx4r@Z9B^LOzi4p0bIc2*YQQJx0kMDk~X(|QBKlw}%2|ptwHSyngGLQ5}v9}Q6UemM#yC8}ISFn#t&LD*wUX?_-E> zk42Vp*xv7Z;OcT`pJJay8&OTx?&;e-FTeBnllhfsWnSJ*#q%Y;5&My9G9S(`(=ii$ z)Mqxp{nnKxw143LWbFbR?k8VAqtjKIi6qmyK{(1cmUt&$Gh<(38dYXIFXeeC!)MH!7em`1S`>q`ls_)gg4nv@ z@v3}EXn7&DHPa^feqFuzsDoO0;ax#Nub_5@aY`z4uEaQXejO-I*&ijG+;M|6nn@Oq zJ)}LSG_?}X7~ON@vIQS{cst$tDKW|t%qA|pXza?#{&GDJG}>sPq9xywH3IR)>g*s- z$(X}^B9rz4uS+jy_Od$Dnb|7&CPgl}y1lsyrL7E|V_&)2u@rW^NEG+4r#LxVRoF^w zuKY4I)7#inyn>~JZGyP&3=?2iQ);So$9cnX9~4N=`Q_D4wbOeCR&Ua*(dW=Zt^hzp z7_kI5j7DX_X;4J1TpkAuxa$N0T%y8P6#V5}yR;wei=~IjLoh`ih!RXwfM|Tv4L$I? zQVTQ3Lxqo(a?xac-G0>@Xb4uD`MLIhD}B{VZL_bAj`&d#g;#nGTh%*n&DJE?I2UGy zdRM85`b9>~InaTAWi*K)KSl=n%7loT@%_uo({uR^LGem;lw#&~T?(LdoCqYX;9sr< zLBG`AL|&CZ!MP(ev?llbdi+~>E2@)hGAFN^@0yLO>aralj~t+cUAI)K3(?3e(*>U? z(uX~q5X7!rV+OS{FJK3?Vt0U=*`=25$0RrGGuf5<18f@k*DN_VcO3RZLVH@y{woK1 zK3-fRCabk_pD4E`^Vs-sQCL<+?q*RqhUakAsq%nqB&S_F^wZC`!md)z9`O9>XAjq{ zI^zev)p~{GCs#_F=*tiD)F2}F7-;by%RLaKnyc0EaWT<*J@QKNut;KmVG#;pf z^xiU`i>uxEee~AQcH>U|-dkX2Rii65%PBM~{Y+yk(N%llMl6}6daa}piM1QyO5#1X zY~X(XF~_$5e=zpeL2)(F+aLrB65L&bdvFVxGHeb<7$0~t-YgizEi$EsU5MtvB)A=h{>DbE%`R~qc6sMrg-f8r1a@@D`54Fu`d zRND*yf)x*?sxv^6OK{z9M#87gCi58Z!9P6Aa)x|Gr5hOQY{I92M8>qn-<(eY1}az5 z@7cR(vj4<0Hu15}+t;kIj7rb{2MDwF(r>R1?>Hv%J4S1d6cbLTSez9VS*ABe=LZwl z*`9VLe6C_y&qX%mj;>QdpYeLaThY0L!K17kVRN8!_1CJOnthx5!&(O(w4IWw@t%g>~1KPU!dI*!$os!pHv-$juQ1krAA5%_ zo`;t|^SMu$kPZ#sV9wlB*zoXr&9y(z#NiYu1v;mICf8TT3X0Ziu89kiUZjjbCCcj( zbF!0N&fUy1@tpn0@AkQwCHA+FeVN(pZp)Imy@S4>J<_oA)&7>!yW=iEYAxgS z0CjAb*+O@y^{|kS$Q>`nbsD8D91`xsu5-RdbTIz1xK_fD9Oc`k+C&foMtPe210Hj{ z&EgvY!_2xyT|)Eig14)Jz`?=76uV&icS|HNnmC%ORUlf(0Q)rzJp?)72Q!GQu3=Ij z$cZ58L%xfka)I&WmkvQrGzjAs&6A(gZP2R-q;i@{e+^?{`R7&Se~|N4WQ_L7Zz`7p z@aNxXuCK+bGe7yEKt_{ns0>Z;A99j?)DHU(Ieo|>qY3x}8O>Lbf5`doNf6{j+x;Kp zgfv2slkDS@-&#JIv<(pv5B{#W4{wT`Tko0nSdt7xt@Igt$?5@$k4B$AuAv+5y%Q?7sjDrE!Inf z-$P8898z(K#D4Ov)9|v%I{?09GdKlE5t7QDl+LxaPU^T02df-f@zfI-=v`W2vnQyZDZ>} z%MY^NMN_FbT|kKz;91#4<1kJdA`5)|Iz*PgL3NG5mmwyU0vMA1R+dy9!|JmJD2`k! zk6u8_^t>yZJHYZ>;M1;ue!s>CAl}C!Pd<{yAWw=ME3pWU+s7fJHDCTgR(km|xx3|X zA1Bno=djJ5wc8^W9;D~C|J&>bhqEgD^llI5&r$^z!CK*x4%AE;7Qr98a{s3L=F2b2 z>3VL8>7owX&3upl-3{H97B3UV(Lrtn*I}Daz?6#$tXRH)7Ovi3ytNFSW1(qZOP-t* zC|04jJL0Tcp=ZILMS<>yq*Q@8)J0G>bH9R8Ub=AIm#!~Qp~>UmOr_aqzH=<8IXb@w zez;eM#j5d+y+$pC=`wUi}?u|ckc#3QJNs>2lvEVv& z+P3Lr)vhLl`Cz|bGV8SWU;Gko<@crD&Mc))5pLqO0>!`RjMMe)9Z99pj<+hw7%h5z zVJ4{?Ryb--bzn(Wk5_vt&DvW-iZR{-9<1*Jo_B~Y54~(4YUXk5xHZ1m=wC^y-RvzX zCw3^!Y;yuJPl#nn!ju%HcH`l{gfiK8FZLR+&ZYcK&r>JO1zU;g{XHlpY_4uMFwykD`@dYESrV-$<^ z2#x}O9w_ls^q~B!A0jp?wFf^F-bk(X<#xx<+0O<)$gknJ}-b#NA30ZNf*7~+s>pW z#d$E{?V;v_u+2iJw>!acET8DziQ9{O9Uf#MdI#m{+Dv4g1E^G(nZ;a~xC485X2K2JyE zFO3%+t#9RCj<0~WXW-`hWfGas)A@Lo;oGD+qHiNW*cNJw-~?2mYxoK)?Cg+#?P2ih zu}fO#%Jw;P@US)+y0}ctoj80_ihJn*I>p?@!nSrB$^A#1NBG7DCH$wOzUe67>Aa8Zh&>`GIwgHKf*X0NDq60K3`^x%5|w3=sujn@u=RY3inF`< z#x5HZ)`!23eiHU$81l7OrQ#bn?V*yuF`XV2>g5jHKqU@(RkGs;w+=a%yaObned;?= zo9{W8b+YGB)&whts6F16@LbQB=-FB&aGxNK4?q?D>W^q4EZWSKPRtHZZyqdi)-$9~`C|9q`s?M{*KWFNTJQ zJHq8?xicl{o$cDad?VFYefEcI6CRF+Jw)u90SU{drFet)Dn;%>2|?@1SLQMBIWQW!QByi5;|pie>{>KG{%! zKb6{j5UMK(;MjyERch$&EaP`5b^|o&fuN69}b7<-VXLgS*cH z^Yz#~gFg=D-art5Y7#7a$i7QgwF^5J^wUX18X*uO28o48a$rQgr=hnAT5rZQQAyAZ zjJl_wDM-+xpa@9NXa1FDptspcGQ$ba(^b70Y3cv#%M-HTB;w}E@b7hI|6R0E@x$SK ze8Z;}A9`0hSKwfZv#w_gJumX<-CTJlS61mNGz)L_FMLMkwf$L;7FUnJx9Y+a9! zEpd})bNfY3dC`yCBjZ8@e?ro6WxUnSkv+=ysbU+Qpl5ObOKzvjC=qS72IbkJ%iRoc zJM3luGH2BzYreFlbFqCnCj;Y)%svi5COJKx**g&`?x7nA9Tu0h#OLkOOn$N{6L z)aAppX4Yx)16|BQDGRW!=#uiK!>K=}vusUW{r~fZ`H96VD%&RUGI=vITdSSsG+A41 zlHB;+{PV9P+T=ZpLnHp-N{uT^K36%V)RRPb^O8K;wqHbJ60w|dKzNeSbdu}w?TZD0 z=2s{-EtQrElf)NEiSWVSEoCo@;KwO3l>zgaB4A?8=J)my03cT;X z3l7&aIqdW#f4lK%)cSSYsclP^U=Ye@kkS1qqzq{Y5;o?i$@=!te0RTX|Cbb-^1}TZ z`1AYb6qq;t8mPBRCaMu(r--0WFB0|&%)Us;29fcm?HGGNYI-i*hCkKC?Qv2GF;bvuDu8s-E@)xQ0OwMY(l?jf%N4Ce}TqOzK+=;npC(dd&Z zczpVUK>Id$XGyR=yY?W7Fi9&acVC{zPt4F}XqQnPwo{E6aMGZ$QO7ZX-BL~efW8Wy zJI|%10r*NO*nzq}cV_ZR?#KzKQUCar|~@VaHwH``OS&;rqM&*6XC>S>YRk zqty)9V23aVyrT@QD1_0R=4woo!MGc1*LeVCi@f{F?28`8G0eYo-E{d?|9gI}i%sQI%&!nNv3x?xdGxfCVoju2sh6^Y3P@82MFo_kvPj=l ze1!$z4mmrY?>Zreh`rx=7Lcko-<6hntPg>|$zM9d_3~~z;e`}u-v0~uR;xN)nj%x+ ze_3xOGiZOiNs@wj`RpS;+J&Z(~r^I|7j*PsoD8Nm2k-P(OXK|QIT!uxy6DJnUs z{nLQ#0J&Cr-Z&~re-S7iTZ(&5&X&LM{KsVJ#y|tDYGYZ@pg+ms%dQ3oxK#7qkszxF zX7|tQpj$6kj4f#wg2*Pj0Fh{)W!49S(jm_fb?TFG&0BKqiykOi?dTsuv3Jr7o?(!l z$X+f!PtnL5A>g0LU!MRh4v{fnD50oCu!C1Bcb6?|=TymHMHk|Mv9fNn8o(NN+6~k= z{v57%-qEH{bAo}nn3Gpe>8_bqd4NGUW86;Y+I25ZNmf9RjX%NTa(4ynCVqf8!fgOs zH>?)2lqR6;e8IJ5ujZ3G>5pKV3XIoIKhl6ezt0Z(Yz9~WGLTflsc!1Or{wmF z>+AM#M*0H{M(6QKJ?Vugtuv2OANj+;V0S&#DX{~9LGSxuy>$(^d58-R48FM}&UT(_ zA-~AtBRbzzNAy4Olpxi&uqLoscOK1+WY3fV*^fVRBl9t(h$5vWB;4|$;UO5S(TvA*l;3^ z2@dto2K%Kzgg~|bzfzS78VOn^JP$wV z+-`6bLU45Gzt+pTJ;}iUCqZ7UuDI`UQ3w_-|Mgj=^^HPEW6J?`e+`r>MH+yFtUf*8 zkFLs^E}lbRYc)V_{HAr6!b-qQ;hi&^S}kv4j{V!nryL$_!LgW%Kkv8NByzJ(as4pbf@-wBOr)u7 z?uxi*%)Q`i^oqfcCDaX{K$R_-oozrla#~bHgbh62OJTeB7vWFIz>XWkaQC*o*Q9Hp z{yw4WJA{r`$@m-bA#pWJ@)Jnj|ES>a?f9A3LMf7B#5BS~Xn)a256?KB$5H1&2H+jg zXw^vb-U-5d>+ItSdiUI@cG|pg2uy8{nVj3lGKoHg4g%`1f)FPJ49{F2PR4h#7<@Fz zcAb+|#;j~U*PrrQnL38%E5TU#6rv+e{ExRSYpxe0s(-{EkvVC5J!R@S1b1}zE%|@v zam#A6!=D13Ja$i}Ap4~gW3^4swR$doI4G<-9(Spd3ic+IufB{&4V0XEeoj zc1*lUpBYa|inO&9ZRROGUZn-)VP!9Bl*3O|(+?f|x7?tN&nzkD%>H6^v@+RT2@Q$S z$FQk0J3ejJtYn16w$g2(9%+jmvXVqUgOYmk3H$!eOOmcf;b)T?&523|HC-6d0VH zfJasCi*^YdwY&rlnxS5S<@7+0;9${4u8^mqpAzZ7nfs8N$|xpMTjl&J?*7( z5Z`RWBuke_P1>^SDVHmI8rK&E&7$i@+Os&^sg@S(;XTaBI)0D%cF!n_6JHGS zne){>{^vV?%koAk@(P0%fOpZa>J`bf{=7t2B|#(0slCWprR`D<0P5X756)TTr1in9 zhpVyy8^9(s;h6-xU;N_uJOcQ2%S$6X`XT8Sa-E`cgPU>$zb&CZwJ9Rl40k| zXf&Yn?JBzS_42%x=cZA?4K~QfrZ-KFx#@XbP0X+0b&%N4B|E zBttY4Ted|KSS9uR+qKZTOlhv-i;LZ|*UMh-4pn)^L5s`8LnqOLdNEdiFBMl8ZguA`i7c{mreSm=_*FZOG^^!m4HlFeF4pf)uu!SVeZC z^X?k+uCfE8{l`|{XE_ft5g^B2G-QnrCnVij_`3Duj_eJ{87hShsEhR^X`v;SBWKfq z`;+kwA@4p;YStvm3lBFc-wKVF;MSf~rnpZ4wGttR&mNObnLkPC;O1yv3!pt4!j)Gx zOaGMV!gp+ZrR`9aa~Fu(2?z$qtuQT^IMLh?f;D%^L^SvKe}ez*_32!Z1AcDW>=xY_ zMf)1O09R`QHEY2KiGy&<|Mn%8;7xI~n;r~OXyvDQgFvciKL^uBv(%O5gU``^h^^wJ zFuUv`q%e1+F@s%4#9@N)74f>UgCbJ4Lnd2mvL%eS_=oAE(sGC8Y@IqZxK=c z>#KUezOvBIu%ZN(y3pbT(z(7l)mHSTb?-;DD}BqeuUiW(-=6OpHy?~~%Fev&T2BPB zD)Ph&9M5Cz`Fn@OVtdoX(S6~hIlnhv}#vz%Rq4-!{`M2P{l&kIt_8oBl z3)o|EDG&9Pf~jx{KP4t^CyUZ;%!ZWCA3@?W`y(lKcL1f^6l5aqek231l46hKP+j}p z4}j6f?2oSiTeFVz?+|ly_0>NkRew~-bB@pIUz}?YH*uZ_+1`lFcwU<-yO^Kw=hK-2 zwP5T+5b>YA6FiVNikZjtubr$HfKZPn-Su0_|cK8 zdRF=)cXvph6rry*RUwYb5vzYV=BbaKT53Nn0GtN-0)ClZLs{tW)T)1y=4a0WWiA{A z+XaW!(3BR4VgV#ivoZe!iKp{3upGDb zw;s!TDCO$MP1ZJ#w@$XV+WAXxjg{m(#PX_m2WH3%rk~k+fvefHH8ePP4L2nzJ>D>w z^q>%)p09rg{%L)Q85Qn)UIc)miYtL>eDUBn%EizVvgthFavB_3{RWX9zgcx_A`Cvc z;9`wv76?dr$&9cBogM1Lti-lj-{d5yxioU$J{4d-@|_~lj`tlI$n68-euuETU~AmZ zVF{Dt&W&Qm&?#w28T9nHcoz>U>R#pSi_4s^*lcN6oFEP!q}$tEQHMCCDbryNMmJs1 z(;ds~^>M_1%@sAR_0^sUXF7LC3ug1BR>|25^dW9-pk3>WGzt{ufdwN?OTky#5{uWE zJ37w*>z|ze`mB^K4*%q~^L>ZWSUBngX;FjqA+yUn-l07TQ3$sosq0BJ(UTjvz+Y$b zHzRdU0#u}v6>Yre^->gN5(2JfHFgKtYUpzwL9;44Ba4meUWRKCX2$m3N-( zyg&hu@A3ymi_wg0IZshWMTE@yLg7{=oj~09bIwzp>ayW6*vWgTd~z*-+u61NVFf9> z73p?!^4B( z#%ty*-LZpWOXI}JEzWJ5pTU_`Vmx)CNyyv3{XTAck~eJ`Z;A5FncEk4%-4U$w>vtlOMM(68>HXaQm3 zyYR+`w6cAuAcnA0x~p{Zur}Xt@+;AOxSWJ8AJq{N55-eE(y*?EGT80pK;awf&55m| z94PB&)CTLkiZ9|kYV-2R$q^BrH%&v=}nIL%gNGQ!mv;!*X15DlC3t}bu zZ4v|{47A#LGxtJ@J|~O9%?La0t=6Etr`>(dTd&tW z{v~sap9-G?My~C*Sc4OQSH-uZl6v$$!%eS@_qVLGNWUZ`av3~Cbi2gTp^b^;C21o4 z-$eHI)Z5Skud0bnABc6Bdg|(xPn5|La&BVfG%+q4`b)3h6dC-f5 zLA{0Zgxxfjz}MP2)h0}3s)Id_vNO2zn7OfF>uEsWBoUA{?^#`fm2 zF34Nd3UG5YMsGhycPMuI^>X}B zM(4#SbA03p@^L5Dj81!ynk{*5>oe0+EYsv-BI=X;ghlWd<#UID^OR#B6jT-&aaZz6 z#)kz{;_g({2nXWPI#BB}reDd$mv+tlpm+fAPKMrX3$h1oGh^lBfy2kd15LuMQe z&O}u%Z%ulRlFeUG@a2=~s?d`~bq2l>CsslIrDJ&c^qcbwragR>o|tmghgy_fZv`~y z#w~v&aoJ%ej2Cz$`O2Rx4tmJ=JqWrmWwPQC{QBwh@jL@V_2O15TOOIedk9aRB${y- zd{1C`30=R8_a8(>-&rh~qQ`{>1yB*(XL`C!KA~A=f<@j~UiqYFYwgvmq@gxpDnOQD zL$OYU7(~{LgW(aWKd5fYt#G|GwxU_L0KQGOC#p>N1P+%_gTEdFFHMNcllr>!zoJ+9 zlB`TY@4Zxch<7!y`i*yWl70`K)FYtN8u<$4rRJ`(1YIVG-x>rEp<@5jOA52|R1?U9 zGO72)Ebu6qGV&`;TMunWR)tT{rdK48@chFQ4mrL^pOS2HsBlp zgF;)n;jTb0Do+dk_??LOgKWX5aSk6!%&i!B^LRfBa@wif%m%N8wm~cW02q!u z^+9jqJd*8B+?}wq=Epv~AGBs&g~;}5`F`R(_xFn-Ly9ux8z zGQ0{I9}~&B{|ql2!qz9c(3`DCR$^Hcamgs8`v2sQIY$$>0DjA%J31ecJfbp!kmXQ~ zr`$$`7x7x|-m%ebf4=0i?u$r___((uhH62V>GS;uapG+4SSs|0WX{EtV82Dy(OocO zy;gb1bYfwPg@DEi>Xd(!OZG=gXy2WceCClw9R7ZZwD^it=JidsQ8! zG`TagAF~SEW)PkVbn^7)v`(W{QpY({;E#mAJ#Y6Tv1kcPjeV420AoxMr zh=s2Y;vhd*?qqflt zn)wSlUNw1te-{Eh@?(LuOSt6duY24o;N%I4S~GT(TD1HH@~!;A-S1qwBdXbH#}E(L zMDk9vCIF)EQlcy(!b6PqC-(czdy=;?If{?W+U|&Z*fiH9xMLNDD|=>-=$FI>Y-mtu ztVYY{Fs9jIRi68bC*gWBbi6{eULc|P5TJc5yO6r3-N zW2505or){ zi|It?n#;p?fuQ_*jBsX9lil?U6;4O&z6O20QfxU1;0X-p|GY-V88hPeRon9?IfBs8 ziL!m%Y(5+76+vwwu|h%^frysY`o&G+!`Y5EF{4P=w~9Z59;Vw$b1pvwM`S6^lZAdn z4d;Z+*e{!PphR+_UtCIscrC$m5_m!znQR3%9Kb2lX~nv+n;TfcRiMk4^ugPF@MrP` zpjY8U`@#$~5ZZi|Yq6X89mkUHw4IMktIvDXBZH^da;eJl|84w#U-u~Jyp8>PTU$Tl zitK)L{X;;35<2X}aWuG>xD^8&%o)A|(A(kXdKU{K9TW%C+Z`?WqQM9*wtRTwX?yVa z{_4tKA*2@Mo_nEV$jb8zS%EksVhdlY0XpVhU~QRhfB&Yr*!)VKBXaNO%tIgl)OogE*i5iW%C|czbtiKrh`?LxFQiYk&48 zV?O_can+=pg*yd(XshY+2l}t^{~b-Ds90#ZyunHe+!&h2rMoFiSN&4@4MJqQemQh` zB8!)$%mBM(Yw4!FH)RdiBaaEvgP(Sx_tA%^rtXet<9Ig=TeLZs$9a8mMR))!JNk6J0PWKarjG*2YdiVJ7?Ir|g==Zxq0ycZ?}$+dDe*L75)DJG58 zmHgY9w-zfNJB!7eMbM);2c1Z^lRK#zt?a!`pu25j-DQtkf(T_MQAJ zg-gf(^$57oh(>cwdpoYUek_fbhcSn6vEsGf?aNI^M+AB6{m9Yp*sx91IbJyvf^xEd$^^Ur-;eZXfarWHlpPxTz#5X>cK3}`HhssT@CDKCql&%X+)e4#}FY2bZ zo=Q14T+u7q9i5fEeVB6SY&Ep4b5cHLBJb_(^?`$=%QQs8Oh$k0UemNYNxRzzfip06 zq{|y5|6bmkJF>0;nG`nZ@Z0sQ8a>rhlV9#&#ula2D{5xA%9`BXPSWz9`xB{X=-i?S!I0^V(Yw>6tIX@uM?UUTTh-Z@KaLYxkil&t^U@$CAEE71GqeV%Od z)pd<=1BbPsV5X7dzV^Myn5$qF_{iis-Hw}~S{Uehal78?73c~Ma)owy*uLL%3Qcq?HMm%blycacF}74>*7Z_sTUgQylx-#>(|;jPp^l~)Z%B`B}b@P zo5pWxt$50oDIOWxNnsBLUhsvb=kJS)&6k8(%?(pD9`SV)``oayS ztQF!UT5{m#76=W{{&tgQ=;QXVo(6tz(jN3|-y10^2%2bVIb4Kq5bsPxH_gk2>)o#v zJt{w~RcjJwKK?x`RnQ|45wH27zak{PDC^C*`a`vObsYY;Zx<{k&PnRJ9`~qJm&_Vu z7aZ%FNcDK(>NW7+V>O7Z|yMtepXzTKne)AxFxY6IrZ zWZ6-ZN?e12uq^O+nJvF1EagKL+fX6G(sbvDh*fb^LgG|7GYU>lQC66&$Cv4u;U!eI5kV5q7$Ch|}?y39pi9^>`0`TA{RMXm;ne5rV zMbgD~eh-;HRZd)eJ=xTMBFN@S@ZNsXT$BpqbXe|0xL_y-;xEaqEte_;b80V>r*(fn-E&qCv}MQN!XiPX`nt<^ z;U^5M7co$01MspJVQeNyhU9uvM6sTfjG zGsapU`VofR0S7kk^rG-*iZvr_PqH+Z;hD14qC|BjMvPDxA&G#^%R&pqQIB*A$FLWS za6D-msU_JqE-US-ZKs?lr{6Zx(~W>TD*2KbTM61uq*|B6%+#(X8|Dp*zcx8kVKS*$ z(~{JC!jLSu>x+r8nZ>6ci&#_H_H{FKSi(1!mvPQ-zZ!D_zUE^iz?xJhOOroFG~?gM z0UfCaK6JmPLH5xm%otKNOIo?uj@04a`FSDv)#5XUl({-os6XT*3vtH~hG&ZtnK;}M zqY$WmI_Mk6_bWP-IT@%Ceqw9|CA+0WE=drVOxM$!i$qlZ+>+;i@s$r%x|_31>c z7J`~^I0-y3H@n?~U9A=el8rLEmd0zC&o!+l-cVSZFX6XLArv{LrA+#spKVfjQCZWb z8p95^%4-;Wv#cjyw%fz8x(@aL;sw;qH*b}!oTrtjV6G!BU(Dr5jJFCN} z{3F1OSa)-Q-IKsqv~eBIIm;f)Z^a%32an&7b-;hVG-R*3n4}21pnO^b<4h=A_=Khb z2XR$aPl`Pn&SmTrzXIKeAe>IxYg3aFiG-A2g@Msu#7LvoE&FjYsI^4`0W6EHCH{zQLH(^mL`t#0*!fv?uu-;JGEiGhk5OdCR{fo^U!~R= zw(84%ijV4F6>2VnOrq5>1>6Vu=vfDbn8|dfc*Rdq+{hz)vUYWTm<>JESS~eKE)67K z89T@hHN1q*(PcgPUabr#&5;L8F%F&R(L&nE9%uUAWb@vQ-gRqcfLmDH+H95;_$i zWw6kINskb0YbMkpQC`1f7x^5<&YPID0JAxAceoUQI!c+9prJv!I(E7G>YeGcGSg=0 zBPTV{|G>d;zN8Iiw-(T1#9Jlngc{B3EjQGq8TxT-UA$0RglX)Ed|ZD(&0`Tw z2q7`6`DA4eH!#iTp{-9{W{VnMeMu#AHww1TJQGW1F#r*&>$g3zn>>`?E*m$>z>#!P zufK852OSo1yuq%ajO1u2&IP<`n6x?4f)&bRgP2jV<#9dI&`L0l1PhwLn1j2Uhn1GD z0m8tSiiMo%o24;o2_Zf1=N_j%m(TvWtR0J=(Ts`TQ^oUZwSRP4kzD*S8B@fM^3%e_ zX&ow1xrL)QX4Feg`tqEcx2w5{mU_YYx0;%+o$T|`%fAF)Esz9XpF<{Wh2WGPqN`t| zO?Tm>yX?P%icOsxzpplk)_8uN=nG_ordHFd9@2@W(XOopK0x1NJjhJA4);Yum1@x? zP!q*7$29HUjb8YKaxM?pBX}~cEZ-u20NDT~_#0db_?8CqoXwyR9(ae}SJZ6sSrNB< zbPN&nEGt?6qHfPV6rgS2xEWq4c++@4iiOOtfymKVY*Ifpq=uWky7uvf6eu*0ggSg8 zzAZ5SRlcQc>g-HnG+dnzy%6y?l^^xhsldy{3328{Y%JvOcePHU$YsT3- zlNwH=V}i0=`?Ia08SP+Q&@3R;q-B^haez2W8*ci z4XdUJjQ}GmJ6DaPb{UcTx-j07R4jDs(HXeXn~6C`k_(Tq(0)fyJPJud9`W*)H=%v)x!os`z5odJ|3xs{8A ziPp`5B;5qHVm<<{pD2g6bZiZ1DSi=}>+yDKV@_Pi{Go2eP3b1!>D*c$5zo)Ut9sD> zfXS||nXf|%r9bXI2D_-iG{T`cTG!X|(azx7&m-(iHO;P`Y|aCtBL6u?XS;2NsZ~l) z^#VT&weCx|p6r}I)fx!JuXnn!hjSWTDe5u5LNICHeu-9-OKlhSHBLkIN=b)gD@#MD zhDkIdbJxSVW2QS~pDRZP;V~UsZ8PvVS%O8)d&d*$tT0xINsQ%0(YF68P$$N{R^u&| z*nYZn_^U~Fb@qhgtx%i`*UsO{A0t}AzbvBTC`dRL=7)5crH}b)Th=)*&@JsBeKwl2 zBzse*bFLDVXLaT8h@t=&Gxs-F($2_znd`^6C4w63{x23K+Cv&#$V+Su5f3HWcHt)T zOiptb65I(k$H!r=H3sg;IJ(g(Sh>{$`f2LJ_kArYMk_Px4Fj=CgmSo&qMfPLT3V4D z6HrV@yw6=V-?u-a3sYGRN;!Ws5sNLveDA+g91;6eS6RUR4erdP{!6hBHg|C~Cgv@% z>M(aj8AY+{W8-YA)q~=SXGLku^(@1gVEMqYOW#p-3rVI*!?6qX*9PL^Rr_2?@3t1D z%)47h6RxlEUupd4BVc3}c0WuhVr!eItMB7JJ5WxRei%ZU>K#+#E|>juxrGG%Jq!-N zwdi@N_Fg!p{-O5XXJKP<5_%GbTqj*h;3 z%gQ?dTevstDM@{hn(U0IhxGpFUywQp7V|1h2Y!fNC5J9S45tZRc;Pl0hs#kSn?2A% zf9@L-2XIYY-pr?!Gm}sr2IZoh_I8&?rv-k(ZT35(57*OMx+7W?V7l!yPe;1|olhR> zwA;TB;U1H^+{=xoJ~dm7gpZac^sbd9?%$_vW;74H>4DxI|Js91v;Vwgw~iV!IWAha z>}yY?-5~NDBnsut3C-AAJh(LR_#EB)UOMRu*Oty7UE1jyxjCthbPhmkqVT37m14gU zt%>F3%W!h<4q&ABwiNcE=%2Eksg>{ehX%U?Dk4J*CCGi`OoQHdYfnyCTyyenxcZON}wGJmz(_Yu)^Wc9>DJ92qq*KL*PMzH9dQ6;T}=D-TdMp$%!du36-jfQ6fXqw(g< zoWkTMoo=l0$_(YN&gC(|@de*?iE8K}^UUiWKDuZfSaK#bsXMuKG9;2_F%NP!X z-#&L_g^^>&i!yJ9=-64J2<=bAaqGyZuVL3Y#Lqt+X17qY8IGxB)EzzeS>`|1a`4(0 zo?1DDZ;UJRq?okRBCWGS-SoUDF52Fv+X}HfOvs1yVxYc;E8C@#ta$3U&)R1s{1&v! zz!_iX;5+B{93}B{U-wqXmtl;9mI+D6*Tx#R%x2tXa0(8Kzqi>w^hmt~I|D+Ag@`Ox zv`KKnsY>&vzFrN?H}4^lxQg<3h*Bo~7K`WT5T*JCqdJ5{-ceubJP3WXMnoirY8uor>La_TNVqrv&3Inu#2l>BG86HkA&Rlb z0-9622xJ3As3q^T9zc(>b3U1xM&y7s!y;gpHG?BSPXjl7P|)|}sMlpDD3{TL9UU{5RB&iwp3LN*Ws_+v{9SEiIKz=sUbtrCkf@LLjr2^hcVTChHne+`v1tgiI05 zsr=eNX`9$B5G0~x^P~JnBgYmx3b_#;sms`>3e#P$#G+gmjGoyDQ0_NU2dpr6g{r&c z*uovv2t=KeRO*lQVI&0Zg{LfYj_PrzT4QG<*WBF_V*>P_O!zL$Rt9Xl3Ca}p^-a1G zJ@Fsh=bSCrgbtccFt@scBvA#J_hL$GYM`LAYij1SV}3L%=$k=bK}n8K$(QQk;v=f_ zYKyz;`NNhukP{&qog8r33HWT5PHG;x%*u@!Tr~k}C0GEm%&tvGYr6%& z&Nah$-Iw>hYUU z3r3N>pS(HLzYI*r=@&oK>229(QoCAeGKUwe`#kOmo3KP(;a6d|zTLMh**5g9Et;br zx*?UinE0BQqXq=a zgYkf!qvM|bQemD>M5h_YM58#y3WyZx>59>Fon*Di$p?k?Xz$8DB1-%*i63 zgLkR34*jyBVxEzHrIg2NnG;))S8{Z2jveFAtSM<9Wi zz&DincgPVeMgIiIS{I~)$U}yn8|oOKndHcJzdg15s-`PpVD^TqFWxDYA;iPwUJlk% zgE7XWyw{wiOf=1TQzQ_=MUQ#-fF+Nk@VdsWx6{Smrn<>QSYoOk3wjh$=Zc9IWZt49 z)<9R~Kl#mx(pJg%?0NY{X+iwLk+s^Ez{ls%ZWsK+%BzH-MX3;`e(F%B{KvyEQ-}8- zsT9}E@p)<1!`&3(@&w6YT0{_+V&h>TF2}`#2A$7BI*a&gisjseW>2oCh-)5XU z#)D6%zVX3+0Mbk~VKtMAFtLy5fP(cdRT-U)&sG#d$AKioEec0BSF17Ep&=*nrY-FV z^9c<+GNQ;GH&gwyB6DA`fHoXH|UfGb7_) zYa}M|&k7ZzbS!?%40RJXBl(4}Omf?wFy@JFipWT$b+w>6_U;2)I7&cfO7(0*)w7Gw zA`i3g41^P*WV0)`wd=tR`L22r2XnmB&I*KL)6kAZa?yi>_J~F1QvYSglW6 z=Ws?Gb%S{uVF+WW9ygcGR7FEX{iY9BW8Y#yp!4iKcEZ4@33TL%fk(Tgkf508vf?>c zK`-&YNPCAUO@gg$v~1h9ZFRBBwr$(CZQJg$ZQE8?y=CL?bH4SjyYBi1cW@^=BX?v@ zG6s=59_)`5!K)tMN@n}EJQXzfwUnwm;QZ{_w~SfJpxGtj>g$tV&tCi}OmG>o}B(B_GlM1jC67IXVIleAXrZ9d z<#0gpluDbntu)W+s!GlK(TZ^f+qTgHljX1TAs@X3)N7a{RGZ4_vmt~3lz)`#kz(Spfs?~VX8K@0UG1?6yxse0# zRA{PP)X*GcVA8pX_EhWepn4;nUQ(INR=Z}s)-(Y;SGwjsS2garPj$_^uK(Z38Lu_& zk5SAo?5UqSoQgBnuI#+e>->Mtj}(oo?wmj29F2Jo{(Qq{ac_bk9@`WzkS#t7{Y$n^ z-(&#~0U|;*Y2_JsDphn(c=15J14OB@$H0`#_Mni-}Hm>Ng;3QXbt#<(Q*1lL|o zWsk}53cQ{D+giUbGh=Q7fwn__5Fh-l5|+(Be~|K_U-SEqxPaH~cRQar9}6fU;B~(2 zpaPwUZUXjG3|jI~iL6o@rxgvTbSoEyOF85Nk>yQPDLQCMAsnlPEZLoLLUQLqk$i?d zr$hFYed7*(0D+DlCZBy-cmL`hihX~u_~cjrpz=M$O~wbRNt?fm({ZmzXvnxdIkZ!!8JfALrH+Z!!U)Z4%Dks z+2#aUvZof0vI>c+Ma?jbX`}LDDTU<2;-CyTHUQ?WV?H#VJ3fkX z?5zH|7cgd<;E$W-$M{UID4Aw?@VDh_@hL%Hqv6azKadU6{FBtCyVloiiN-Ct+IQz;w8&3)72} z#*Ho z$G<+cJ5Q@hU)kZj)e_+?ZUA}w#q(YtfaaAsrrVcU0P|f?cGvGy z#_8tcudfMrlRaDA{KzbEsy&}KZ~A%PpD+1-p6@#^jQ{>tSK|Hs1xo+yGM~4z?)n52 zmCXG-y*20izu&C@+<^6Z-|tT1>+^s7F!#$b_XK}F;^)4v0GCADLnb%BDyV-~yctzz z?8G{MfvVj8Fz#4*>(G`0a6;<;j)ASJRePg$YO)`9=kBz`!@W zwn7-2B@%>*1+h!)0cY%-tc(S{e*J>XUHC>2`c8v-5kN;g;*G;8i0iy#J&yKIru8>; zFTV1dGTpFIqdZ{u`F`M4Z(e4V=5OO;!W7i&An0?M!vl76%GViKd2+*OL-5KSskPkM z{qe-X?DQ)Szcg;%oZ9KxZ&JkApRAoad7r@?f#%!K@^91h$6Kwo;pAiVGGEYtHgF1i zfz*CipG{Lv(Kv$byKea$Vf;v^k+a*u*`(g4oAOT@k+6@P^Svsol;56l+$!4GV_m+Og^Que@ zajWI>HrK=`yQAv@zw}(PQp;KiVY==`ZP^nxIq|r1*vg0GZ|1xPXh!cuAp1?7T2*5h zMc*7ip>`!Pg-VK`Otg^4w>OFs?pMg&0s1d@F~vCrs_c7IoGC3M)d8-1XU2AhqWt9Z zS!(H_oW<9&9jTRLZpZwEagD}VODx()j7yR5N0vfy^1M*CQD*}hVi>ygGn+{4O4&q{ zKl725_bhIk9V~VCSPW*7GE$mol9^;$QB6sWQcEQe zng3Lga?Cw&X5}YAJ{2jiSEv}nON3zBG}jGWkDWB`hG0lsMks%Ds9Lq-kggixwU^0| zS1*@nEU#ii)}~duWXk9mvgC>th1fDt$83)#vMH94f(zA>TB8+3(a?yLt|7o(lm*%P z%-ic?C%sTwD}(6a*L-BrWZ(h{lge~sm-6R3RcC4k-xa~7y+C0Z!tLQEm{oC0ty&w$ zhblyTH81H4lf}+(4REduv*z^X@ z3`KUT8g=c_>_Udyk@cQZfV6raEP9s+!6zzJuS$ZTv-O!57I$Rb8h{NfdD2K_rzt z(mQG@=sQ-Zt(%x{0iA&H9|!$ z$nk`_vVUvRsJIx#n^l;rF0{j2O`Et}u_AaM_|Fh%n74vsyOt#GiRZAotS>uq>NBZ& zW?vRCMG*Lk54*e13P@_jks$~iN5Gs-oLg{HZvm|D)^s!3~ zA`VFB>n04p2pZUn%-Gg@XDrBvu=ek7u>!nG5cV-)ov!GTs~liBDnKq1vRKkf6K}V8 z_Sfj<=@fxl<6Gz z+@V*X1$_jqE`#D7wPFmjpPLu%Hj1yEckbK>Gyi$d;XNvpkRrj+?mjAr`GFTS-h9~Y z`w|$MLZ~q%s-F3x$3E2$8~pHG_cD&Xt6MevkN|q?B|hRI_sln%Jq58n@`;47S%Cs6 zV!Ht4#b4&N-=**X%$+AlX8VSpKgSSk8s+#MwVdIDakYk-@PB`;?ul^T_7aNJmsjCv z(&KkP=-=ZQ$~(HZ1yaPL8H|Tg6wlx85ELZltTszrJC9pL$-s7Zt%65LZ7uQdmaM`23w$KHZ@r4{WR*q z0L#FJkWMtXWP;=o++s~rOlZ55rPZj!m)qYx!MopR^ok&GNut4km?GGa=|XbAoQ0#o zb7h1rRdj6m;R+RnoVr>{k1kzM#etMyAGiXlT?A@^gO7S8-UOMRLfyGmIjzpF=R!6+?-B@Ap`kks8JeoF~)%2<=~1K%eV zOk34>;^k`tVhu^VZjJoR%CCdDsd{Z5+A>DoXUn)pC5?Xbmp@M4fN?LH`9itYeBTp~{cux|i{(85Rc5dt^VL)X zU^%A@zMMHZO)#f3!VL$(_25OPtmcJAdHH9VzYx!FEl-GuaX;Qt%lWddmYuZDNbnHt z95=50_d`}^Fqf0|Wt=>0iz(DHz|HLsR*4LMgR zNGffSYs7?gjco%J^e45nP72&92~f9T`4S5#OfIa(B$&cJJXZJa0YWWhRI1dU_JUk+ zra!HNCrWCImolen>+MecKmCnW0q_uXp#3oB1npb}eVzp6so=XkEGY9nc1T1gkvY-q zC(N^<7l0mz1P*mjJTf&PrcZmscw7Ta%MgZ6Py$YBYa)7SH}}D#{$Ox|mXT<;I}e6Q z0>~*3zUn&!T%~3_+#L7$q$e%786J52yfBSUjJSs4+^a6 z&}v;cSR0LCRZ`r%-_{@KN29Z+Q~w_y^1VL1M|vFSy;gCuOK(zqDRha7FQ-!cCDzK0D2)U0g{cerlMybs#rTc zOkK|n=n43Dm=YQx>8yYwofS-~q+IN8n;RM5b)&I>uUgRX9eIE*er{FIyS?z();ZFd ztUq+{Cd>X^$<2%8e%Xwz&OV0U-kI`>-#CPhDBcvI^`|jB}29S=jPxOh~}@|V2)*6Xi99) z9FPeWO%TMSV4K9nH->{B652OWN(p(kI`RmE2v)oXWKBC~18gNtP3U@SaN#Va1~JV- z-BYmbU#Na2tfE1oRVc)PeZWy7{+GTkrkf{vvc@}vYdv?CPN`&G3pw>PokMS(q5P!t z4N@4SE@wxtX+rUlpWA%W4l17U-zaH)S5l&uF}vtgsr#(gtp;{DYCkh zbj>6aQU*2f4$Ba)4D_tU4_L$1?Pzs$0TZ%p4ts-Qn;J&rV!QWR_h)IIB5Xt?oJh!j z=DzqA_|nA=s>*?xB5uOjV_#bY4pTYKTaoB86{VUb;LtpGg~_JF*)$^9b3kVK>YCQD z2fnG+olQwr5zk`$Q6AGHtAxRJtZ(yze^}ii{e@2NE_Z?@ljCsQsh-H!3-q5=2Aq03 z0Gch{41mK(52?9j*uN11>yin0KD1AA_g>&%%(b-zt(9Ps;UrT+MpFh9n#56fqM+nX zl_qhU^1u%wjLA`4Q{!_A=crGl8tn+?qDi97N`s<=o~3L>K{BA|3T*Zw-JV*i@b+=I zqdp#0I6LpQ=bHWb^j>8VZpgkMY{5PvQM2r~_P=rhfwNL~qoP4!Rtlw$`&bXp!qQ&j z`dIykk{M`9>_G}eL6*Xj5=^u)$7}Brl^IMI_+-x%#QsW`zN%0hpM~veD}KqXea{If zLW-_|`x1cKIR$CNAYncxK zaOZwIAeUj4>cNW^Wj*FNUpySjYxgNbd+3kQee}6Dm6539GGS^xaK3lrTXG77(C>l7 zGo||T{$)IPBmFrJrV0fi&qLj4*Zw^$7RRE}8vOOcbF}b1^WBTMr=>vB=g;C+9BuM6@jPNtyNiD;CDo5jjY>1Z8PC$xZp zf*~vsS<3=5mDLwTVncfet*myQOJ?jeWa$bw^uTl1TS8PFdQ|%z&HdP;16AiMNUhqE zI|tm(OI}y-Y{n?QR;C^OP3`q<)DEMP5b1J-Uek%$ZGTl+h+MvI66}*9(PgoNSu)&s zMnAj;me=e7L9}tJRvvpMrf0)B>0BMJ64Y)qDh^*i$a}oaJM9$-{$|CUmg#PM?q>FU zHIjy!7?wq%f6Blu(OvZR!jA$fOMeCuPcl2#7BOYUe+>Jb_itIajqcCj{AMA%xuQIg zUW#PW79Ykrgg=Du_-V7fAG=8G4fy=pogA_6dW{7ej&^s`7bea07-Q!~cEdn;Id1S} zX+@mC>kwoR@E<(#Z`DITzNDI&gAFr`rUC_jj{LbWHzfzS=svQ!ZJ zi6-u*@VIptt3_*T#P=9%Z`*o*2j~1ss=fyPL{Xf75A)&$wzrawNAqYs73=p8l>fuH zhIm4v^a3-9Q^&q56i;nM9D_yt;I1K>2#*V%Bx@90U4eV-a~?+kk-|@=#F4R)WWng) z&ftwDGHZ!QCNs(?w}>6k6Ybp6SUC?qGCY{xE)JH!XI*RoBEn3jra@`0xQ~nmonv>mDDbs4ER51iLd7-aWGlM zK}p;+7XKZbRupj4P-PYe{de#$ckg2U|4jV5w=(Sce?9v@uZ=q1;i0ohfX^uYa#orD z|2fQeRY8(isY$zPtJgXCW2ehi6Z0zgZBDs)3J8B8{F;;aVe{`znMK<{`Bgg z>7(Yj`dY>K*x?kUXWVgff8KM;emWLN*kUnbcELx)B|01tL_QRW&C>f+22)BH_NRRD ziJp71vu|Tcsl2{5!FcM^ABU7q{ys>zYVK+AhRf_~_h{Ox$y>kn(*!2sLymz2G5FlS zDrfPy?b$i?(r$Ms2QK^v#jAoY&SI8-1<$0r2Cf7fl`#Va2)oQ$DefZ+dy1>1Y5ZT{6(lxRGqB=&hxt!xhcOE5++Ga#T6b!?o?0tc_f50D&mpeg&K#%`Y4QRjZgU1|M8pb60JpTinfJmPAtL3xE&=L~7 z>vk6qgil2*CHP@8kl0N*UAc9sA>WO)U2%7FtsCF0)@fl#cYQhReG$-S^t#fTyCYzF zm+Wby$qF`t@z@O5_4P13XJ5;bA0Wauw_#W{Fn#d<9kE^e;kf$Ogm5=s3+#LS@yn0j zMD|@94syVJ&<-zj{v%}aV;6o96_vt|*5q-2|8<)*&qyB zY}W2J7ZkMHC`lIxY*lrZ%2xcB6$--i(B1~Lp}j8 zu~tpqP}2kb8P}tjT+(ENa|KIdA|9HARM)(hNtg}>qR5VwtNkJTuVbtVrPyW^6Dbo= zQ3|oRK6=M}@cI-a+4gEq61Pc-p{&$Sbv~SWRcgwOgjTI)T{)b*ilS#!%VBYpP4XFe z1T+r<=L)2vbee#Cb7=Te-T9hJ0Jw-0iaK;@V?Sqy4r|qmfh&#P0?ncS(9=3Dl+Zlu z4sR$qc-KDn8~(sBu^zgduh^H<{8&MU95m|1ArpDbf3bW~GA7)7I&$h?STkI}Fr7M% zQW-KS6*tf7wemYkqToU_lsJ)0$inNUc!8C6v>ma&IAc$G1<3srH`)rh1R>^59vnQV z{d;&E%PkzfI=Nu@$l)3C_UKzIm;Xrgi@X)E&dZLEH|rK~<=Zi_qqpl!95`@2vGe`h zo;{wbDZf~8%VaBIyQ*ia9u4G~9!(V=D8O?(hfL=bd4XAyinDnEVafb>#zqY|Pe;aPRVFe#rzqCbc4%#ITFn(2eQer#B5J?fdhQmE3;$md)KSFP!LMz(3$9-zh?~i+KR|Jp%c8h2p-h1 zlu*UXWb!6~`fPUL5IVQ5`@PPeGla|~YmPLmO8197K+Q~J=q55EGJ~Qhj`4cQZ|RG7 z*4ES9;V;5j4U>}5xS@k|(c%0D%iE$n@rCbk5cr>g^ReUjg!(3%fW9b7;)~Ag;qU%j zopZAtovuY3p!HeuAj1!v2Z6FjH^nedGDK6$5T{EV5Oz?=JQ#85kVPrUos;0C@AYOw zWv#Z{r3m4At|edtX~jm#yHRjsq=P8g@ zrtTz0--jix-Hu~HJEH$p$z(fqD1pwBe-7}r>mf8*ltUoK0+q6o6iycl5cX~}$ZLmY z?lNYQr_&M4fc!B{XZBb~WCxMgn=ULgu9K0xW{Q=}H#+TQheRT|IrL_x2s4%`d(&wW z*@!)$ZD=yA)jFu<=WL~-81la^!%z-FlQ|fL1|!{amFD%?tNPQ7UuQM8A?NgtC-qu{ zYo0(==V9CZA;EYtf&7w0<#w0gEopK0d_%5l9t4$q9>aqwbS1WyNi@h7RXhtem*bL!JIoUHAFpI(yRy`Z^hDvsixkUzyhogW=l^KIYz}=zU6H;VU}hS=W8}` zigCp+&CzVa`o97q&Bio=LH?Ba@{hIkS{E%WZo|*p<;-dtf-kI|9cP~(*9I;f&*=|1 zm*(@AKkzTg<{v4RnRWMi zEUg`Uani*cqIA9q_^b;h7hvZy$VrGc<8eK;dTZ zQAC7x=i-7Y5IA-L^>>fe!o~Y7c5poULyUASM%-`H2S?#8#hNAeC9B^HzvgGj^$m{c z=3>iXjGwEtw3c@JKDTRPsXg+SaSzgO8C9DeiN+uHePtO;`y%ytoH5pkUccVq> z>?kW03tF_(9W|u_El}3gbD6Y^yRL@G!U#>Y$%=I|mL`n?swnByNj}EPR?!IYwX@`c zrEw-thhzv{iyK~ai6WG66rg);bV~ln_Unp!H?Z$i^=4j$S1?=7dv?j!B*j z12w}{D7u?MbP5!Coy!~~3VKv)Dtt)-<4kg27r8o|oMhgl7JDB#)uem5NVj*No4p2t zlpFC3x5Qr4vf99*zd1g-4Q0$lWXankUPKRV87SFH?lNQ~jy6gC#Yib+iXpq&s|d>2 z=@zA(kG9GB>4!*;O z!4o3uYO&P1qO{mjd*RzKeJGnlP0K~5ibdWW{T($)^H%|>l108K`=$RN93xuNvy#e5 zI0XqiXY$6u)JVtVsqWwRRZW^IXo)F~{}j+tn3zpq{UrP@7>YRW2EqH>uv%}MYAG(Z zVr8bpdIkctdhVJ}!LsA&>^nd;m2l*U zl;4MwdLHTdrF9SJU8z%}#seKvQslVl3&(7z3|T}F(@C)R!U?|GuLYLRdC^r@=Vnqow7P;&w6gX{->}8L3s7F!Em?4Y+NHi*|O~RhMxUC7PyNjoHc2le3 zFST9Ctup|j<}y>Ri|}PK(VyEG1XOT%G1`S@RbFbcCzJ`GHgHGab~m4~^5f{Q_0KP! ziPU{pyU{XF8)8Y~R$^^Q!iAU7@4Qw-EO-xpgUk0=F5KTyxT& z`w3lDT6DbDY9qq4mB5Z*D8@f7_q$VZwaqJ&6RUDo^)!%wb8FS)8$Av!NxUih1QW0_ z3p;ZysMp;{*)t5Hw4zQbu2a0FcvqF@>_;h{1yr!f$@tRVEtyW*fE=dzOn)cXBHlUd zc(ld=%5ZOYWPr%_!JoTjjGo7qQ~OI^zpXCoN5KsYqrs^*D4Q5S@~WmqcimYZ2y!jy zYF58#D7MM=6d3HYUI9L_e00^LZB6dAchc-dXX#pkkrx!GG~zhqs#I8!c>arajLJ{S z;vtLFBo`PJBP}=8KEQJzwe_wOFy#PNx#CRM@(^|K8sJy~bIr z20c?;<|WFqpXnV_^lfdwYKIA9c0pHqq_j#pGT!Wk`{Lu_O%wYi9Z-w_PA>cgELAdh z4fIvGEgS$zdLaGBqf_`X)NFlF2ujfLp_X;7?gbAj(LJX=rk#s_Rk-S91M@csLPHqk z&SRd~)5@iu8(}n4_NqDtznId-R4Mt5t(gj&<9jhR9i`I5`ZtN> zcXCEh65n!C_j6_KRuS!bv!&ys&U&9xOS*Ty6h3vJy%uXVtUE*NMGuh z<#(GnLie5FX!6t*4DEq$HgxrB@60Q}WG1asaWUNdKI7BHSW_CYwG28sc`7Y0Qog59 znb_x!Z@q1`qmNM*#i$A4yBCrk#n6NyHo1NE0+dFKT*?%Kif&R$KWrGvS=kl24)<6r zBOP8ZJ%C>>1^vL*oWe`TN`2}Ilpwv6#wva)q$8x!r91g)T{O5^0ft3Q<(yOshw&+V znE=6JtHEb!r}bYqG;3=ErXI`~S46GWQBuq+MaX!a%+^*glA_O3_xo|xjOMJl#M-II z(M$ljK_Z!;md;!A%8Kjr1Hr7O&Kp)}F}Wp~c`vt`ANcRi>lFQ8HS!&@I;1@kz~!z^ z!&n>CWGL9^$g?l@O$Z~#RmEad`~3y8ze?XDIKZnFRsRv+b*_~5^@2taU2vSKgN_T( zWKEcO==qQ>a4>=;wZVzS*)XA!tImpTbzCF}ZI#i21vYy}ct0#ii?Ci6IOLvuS_1dD zmYG%T0yYswHt`Dkug|lIH*$ zKjOwLZt=_|o z6*TP4zi7MpjJHf_U&H*x{LA?vo+Fz-BKA=$Nh2`eXg;}TqLpXIF3Uo^tPaA%DrV{ zvB*oXYtL44n_n;LaN;31e~hFYQ<1hsmC>FVhQRs-@+1^BRycPZ#V@M_QiL5gt*sDmUeH6O~pyvgl6ONDO$f`nnSSmq7%LaPE_$E^nt@AEY!Fe zl-_N_v{yZBxP47UUd`=!b+>YKe73NN%Mg_-e&gS!a0WE-4?I;ay{Mo?z4FFTRd78W z{4Bd9sg5T_E zU~3mviJo@InE(UM>BQZMkWHECRBmLlh|e(y`?g1A$jxcEl_xT#W>uWZlKRAGxN$@M zmGc!p0CSA`ZE&urHGQ1x{-ogJul?V&p_$ix{{#-zUv01iZH?c0fb?pSb{@O;T-MOJ z%)v961N|u3S9cqmce=G>_q0DA!~5$mD?b8+%VcnPKVZP)6y(xth599+yhCnKM;>)awALgtdX zT>ES34IW>gtT>E)e|cZGb?>6odcd#S>BddJ794@neRFqm%N^&wWVhWAV|(hDU{jK^ zOBv9we%$R0VA}sO$?x-`C|0*S`Fn$U+mJtj^Ote^SHBdl|M{GZeZ=`f+0bvOX#W}FMJc-+nAT@(xhB< zxE|*$B3y@YUa^nj5(h(JFH7Mt*#`PU>7YOGGx&Qt;z*a<2Bl4hvuQK8_-HT@vBLqb z-Ig-ZUDaZS#j=CFBxRsBqM}LUDsxZ}l}2Da;=ZGw^)J+kx>7~X&acfl;Z6V>QimLR ztRDD#p%>113|ecITE)@qK8YpChp!zSv8%2jC~MK1g?Dec3BKUoHg3e;ndg8PdJ9N5 z1NgVDKW1Vrj1#+iY&AEJ5Se0Rztk*}10m7c7~qN}&IcXuvvl!xN|3t6riT(1b2r-P z2i}{{!NQ*Rxb%6R7*Q^ewq%l8L>ujc&g)6lt?Ay*%Tfs@+XP)-_DdB=h1~dJZQJXt zhc1@9MeTNl@1VM43MlRBB8U4+MP-A@%CRsvq{PAors*3NAWL;P0HwIS4^wgfmJq?vjbsPm^&>%2MdKL|NRb{BnnRVu$w}9=p_b zfTBAC1^xzYVwM?06^wIb=LVeEKTS`4=w#;`VJJOfj?O}+wSjLm^DFYn!8<``fVd)B zL4>o1gYGz7-$f1QkCn~0R@yb6M~c3@IC^|%sykvs`{3P02CJ?B2khejdzKuOEKtX=^>S=JFlHA3A#Lc+w?=>bOf_G z3HGdHnCCc13^8YdQ7K-*68QW-URzTI-NBwPFoBG$mI=bGk}B}qYzS!kq^Tl}sey=C zRz%C7l#-ZTAV!qHN@)+|SKnpq1B zDm^?$a2gxN8Gav=OpAe@^FinVufD@1ebQ`JFo^?S1KUod7{JG0YnAfIJ=#S-Mq7(x zD{Uz2(iy4iLzqai7!FLaoa~aAsC6^+2LA?~K@6ZIrOe^Yak7p`TP-1TAt+R*3;`ED zN{^+*UC@?;zeg=6x8A?4Z$Q`DK!6&>t+)4?u?B!#A2uS{PW7#vx}3!8s9~STie1o;+aG7hIpLe4Ky1j7H}18HCk1oIA2Y|E zKjJ$;SJrUX1;->%?}tf&yy<80NLhk4AO4j4XnIAYLl||MU%dcM-dvfz zT=4gOn7tks<&vSk!)5uPia(e$czj|n}si<5b$OKyP6PCXJ1TH+b~Sa z)lN*4SX3d6WzLa}1k2<*cX`&Jol-3TIUj$@$2&owAbzgXoT4-*8q$MKZpfmMz-0fp zxu`C}8^uk!*)r9(5Zqg1?azc~_G#1XzaXm4noORX)Ap59W~sQ0wpS9z4VJ&eMjy?4 z$`+amZ>_4q<=f@fS9#i6b#r}{b)q&=2_LZgIpVk z&$8tD#T8)AxK=I;$hu;!-ZrR{-#fHSH5F4cmkFIn?uA1H`OKA==5-8Eok=9J$BI`g zAm_SPEm*02iL?)dV9!Bbhu+4g84l2?INe3vxzPRN{o?9y&^z&Fne3iNQ0E3KZ!&hD z0fN_rP%>+5RlAl(f?r5imOFi6{A9OxzpYJ36Ifki^8&aO6AK-apCcUpuM4&3? zuX4z@Psf3ce6?)Oc!Gaz0;2QRh_?X?#UuF`XM(-@#x0X?ZLr#Duw8l~x7j0ajF9XM zz4yQp`uX|vz3**;AU85UU)9lH-&QZzoO|7xH;i=;%|^~|Gx6$nUq2nyw2)^H+3=sQ zh(AY?w3h2nR!{JMZlBiJ63W4I=66`T`!wGUT1M|7Mw|RwH9P95SYwmouNwO+q&9T=~l>EVpdp4Zfkhqmc~7;SY8Ul7jzRQ&ClaciuMlLvf{eSeNflaj%8 zHjIR7{}T%KZtBxH=6uW;d3r`N%hI;3Uvp~r6M7(vchO<0XZL6W(i^C!c5XJWF{Z2x zF#&(QIfHHcVVCqeIGL^n>(m}2K3wOPq>Q;8wdO(OuPiYQ3@G#TbZt2S ze=ixcua{P$DBQk3S6E3y!?9n1$(Wv{b7=To>7+i$zpqa5;Y^xC#lnJ>uQ34z8yIM| zQo6jafhx)@e;FZuVC8Ao?BBq=fY-m9yIFg;cCF8k)*R&5HlIKW0&MOHOqw_WYfPBuZsCxg zjcx!;`@ww%&J*_jm2VJnU&}Qh?JlpF@6wScbc(Xci^l8I_nA)~HZNV^C;I^+aK+om8tEq}5<%J$9n>JA z^86L^!dw+|z20AJ1!i2vTjU1N!dM^hE^l6MX4jK|gY!X}@0lBhSxoerhMiMs3x^M2 zAj2;+|Hk%XN({_fsG530a};@ebCeH9KfoV*^_vs7=E}>3yrB&dvX07ke+rVazhys5>B{Civ zUWW}t2%o-S(YoLHm8>1FQsPQ{kxi7s{xjQ(jM>yN|Lmd*8cbWk(x4>p9qv4Hq2^H2 z{fxSug>KuiZ`9RwNFjm32kHbL_h$g$v$)7Ryx~9;LUdXRX)Dbm@X`&J9DAC!n!S8QHS!-LWj^28 zvZZ|HqZ%j}6oul3Df;6a(lXV5yI?qgMYYLxDK6D9_3HAej7FGG(+Fil;i_+)@EVX{ zA(8~BZ_Suy{~~TEECpqI-L;eEE1KTQq5p3BM9)M&)!e7*Pd@9@x!LW8Q2P5vA9(L! z_kQ!}lKr|(NzXIO_^^@KtAFT9pao8HK#UBdg)=w8Dl8jjI%g| zXvkoVj7mK2{tx0p!99Dy?thGtVPK$cNQ$kaaQa({ypb(nLIYqHJfrv%oB{()fim#Y zGi)n|L%Xg}XZ_m0xE^qSCcJ#|$&@8&TK3EDdEid537mzY>I05}kDf=1WCF}xhmHjF zZ@vSKYxUm@A9_cqi5P?9zX~%68>Z*{ecmAhE_1u~3RyXl;5nr7Dk?%_i~NB=`>H!| zP0)Z?foH}N!`|kD&!*w}H$ksXIwZ2B<5Bk(5Ih&4wC)}hlM8jh&APEUQFO6B1hw!? ztd-bL0^{lo=psHW9_fjXbFyxgs}V;@`owF%pqwJZam~9(wEW_zxtfLv4N=LJ73EV} zD4!U$VG-@*H{+toBUAwgVuSf>5l`xb4kG8@8;Ad+RR7-o%Q#sJ-gvTEWOCEpA2f5r z>D>?NS0z1c0dz_>Q~25yy3)Q`BP~3U@(vd6yp9WK&FWr@N-r%W?Xt&YWKg_00adqg zV+*S75>>>q6p26gP;Bpg?&;wX>{89Ho;kgKTA`K&LDQ{&sf`U@aUz23{2HOyCt*#M z`32ieC5Wv2eGO6wj(|gSd$K!Fc5MUx`+=vt4XWc14d@zalx9woZ0|0juk9ncf0=GJ zuvpKx&By&FT;>c12>GEJaX_Sc-7)AjI^IA7_fPOVaVBZBxuYKYQH!`@7AVO{IOub#c{K>L8GCl|NGKj zKuYmvuomLWb^iD9SnF#;$1VwEJQ&#R{Rl!L9KMM#S;V0f-b%7F8k9m7N!-)tkL}CW z3Ux!3pqJnv6>-wMshHsa}vxjS4QNTJZ!2mM&rz#EVg$J3Aw&Is%KqEiUYS-CiuS z%T_rLC*-!T>iZYz0XUxf_;gy;*bnggLIzI3PLe>UB%?NBHQJ<-z{I0rx=7MT6FJ5@ z&T)JC^+MkgrJ?yEy~g-rwKYU4zJAzw#?2#rU%V699s0Io%c=D zQEK*h0~m!X7A#NkVJ+ph0yzal(wO}r_Al}XL|nPuF_nBqOF^iAH0uQlYR|e?$qp{T zIJ_FWK-j<(7BzX5E^HJBK>7GCFt5f6u8H8pHjY=OK-gI6|H(Yr+KGke>PT>qEQrP# z?9*L(>;6a!kyo?~=D3rwuNA{}4IF7hvD>KdJP<7MPRo7=ty>`^@P4ZS`po0yz(;9&{nXDF{CF!T)c?A_9P1Ml ze7T%6BN#9486#-Bu>yC=KW@0`;H^F*K?My?8c3PO1ee3u zpv$M7f%>e0Xk_qqCE&kJqN4yXk3Zfx1ZQMtoS0mRcKp6yiD&fp`P!aCoxrPuWn572 z<{oj&GJ|`uF-PMsGh(GK%NcI{Gv`Jh2Sf1B zz{vs^%&T}e9Cl1p98j#FR%T>>0&D);DuxIO*m$Jp{J`PbD4tJU=YdI1bn7ClMbs0jGc0>On0%Wq7QJZil@qSYvk92H#k@82j+ z9^J<;Ai#II7uK@|^C0mFDxF_<$!i{-hq#Gp_%~{*n-PkkjA1{1Yll!g1Ng?aNgkn? zTGZ)7;?0(18(RqU!34I9mn~StV@+gR{|x-an{LK^l;_&B|339Q>6+1)!|(%AMt^s7 zshF229cv?yeeqNadge!(`neTm{@oe<^U(LrX?;7_V`jRxYd8kanKh^(^9+UDD)94! z$h5l2YYN=A7nx;g)m5dV|BJ9>)W%Wewf(U;DfxtKBPm%FWz8TQ(k9#(D&Zs%Bnu7W z)5bn`ZqW1(4_h}Y&huUSdsu?a=Jt4ylT`qbYSYPQ zXq!O)h8o2;mM;Jfw}urf0^7mowOj(?-J?CBukDeONt5)X>6Jsh7sy48OvG|c@QXW? zbS4`(wdt$8>NUa12V=snwU5Er9<*JPk8j*XQ$N3Y5fJsT^@v%^fSh&>uS4bpc>TN~ z3CzzThua|v#PNj*Oh*23Jg!b~Wli~N)o1DUu8rxF$+#kE@quKn@Py|asLT1{>N;Pc zfDb$koUPS{^#V0Nh$0<1elF3)$g>W?TP`+JErZZPElpp)L7yU)2+*C<21(kh_{Wgy zx8xlHNN?-b8yL4^ahaT=P$^GtJN4t}r$}5SOIE%++tS0tlU){KQ|kz7>i`{FCZg)> z*GG?DFV}?^UxV0jQOl_oNa_0)$Aet=vZ6?cy+=2j?GoL6fDhX4eDkz%*nw^2%g{Fa z9ysRUZTa7>cO*S7YZC?4LZCAP#N5lZG&SCK#Yu(|%{t@H2;VgK>kcaQWdZ~ZQ9v*(R z*B@Gb)$FAKXavnayf|m$pn$R^f;vIO=CV=M2L%x+IdTWGbxH2V4%iUKjB5iCbXb=A zAl;uaN3dj3{}Ma%oFr{19N*;zU~EemyIQ`vZn^87_m=KF%wYisZ|DB|HT1Xo@EDhn za6@M{v=RaGnyJ-m$_rkr0zBvp=|}!Fa&GgJGmfX_ac_-SUMwefaeu!P&zHS<>br=%<^9^%pYbM!&8XzruyIhmV$f`{`SEP_b zg8?ev?b_k;T69k>!UB1gO;6^RbOJ3xRQAN?9-Vd4Trs zd|(Lq$38~zT%r%5$K&qTC3{WR&(m{$e3PALRB)a-e@33KEoa8Ip@U-y$$a?Y<$^qA zgXioO8yq}@>ZuC`ErbCz+CZY_Y}ap|M|};k?aig@{c^SQYop_-YXdEeMn!m4dt(be zKMRp!{ICE075dchuZ|Z(^U|jE#^W97MDfLIa(h9djtYq; zRLxI&vKXNoF`}m=J!$TpmvI69>h@oCC*Q=}wddb`@lETko!1L{UCc+FI!S9C4*@h< zQOwsljOjJ2RbBa_@lg(=n2cYNfL~rYSeWuTuuPlxFo^S372JREu8-%;i+fLcaxZUW zTol|rl5V}~$C*xb)q)nhZVK5!EX&4GZ{$K&J482pE%8-BdyBSmA9+JchD*3(Niy9o ziOYF`FJRmSIW!SJTf0>w3*b2>=H;92JX_gvfWQ@L%OkLt7UdXT7vE!vd^fuuG8n#e z8}xm6f3blkb{Av{@Pk8-1T@W|76jFY#O6}f^#vw8C2kfSNkJg;HWnB!js<_RdG#9&e0RNCd$j^R+*(7s z--E~s09#LrZMMX%mfnoJTB0d^w_74m0H9p4=K%Fj{uh4xIzEB8xM%3qXG_*j2b=OW zX;?u-+wITpVMo055@LL!CqmS*LqD`*E6M4pwz)=U3C$11}ZJc^Y*rqzPB@&m7l|*B%pIAnBzxT`O739aC98 zXyNMsdh$%xmiGXI(G!_^jX2zT=V};0X?(lESQT<3X~Clb3EYovs=g9hXpK(vM~Bge zaZhLdip^>^40K_01l=0rkb-Q19_OB&hV#%FJP%z?6-c3l*3hHYh$FA7`~gU{H{tDA za&0`ayxRaPB^t#fVd3@|7O%hiui@-W1_k9o0mk-{JN8;JIs@pQ;rPh(NAT#;)7>(t zjanK;WGLubYk^~I0Fc(>#e4V9XmX-Hf({mhMG9mh2&+w?{bjH_Cg1NJ`N?0eNs)E1 zE{`5PX-P7)+un%7ZZ>G*VVA4}apw$QeOkhBZuLG)9`ehyMcj4#Qv$ zTk#Ju#jGDY8=1u!nps|S=(X2XjE$S#pquBsbi@mK^_k~jeCP^kklR@PY{{koSbt2@ z01$z$PhQprb_IE^3yP}8nfMDkU@KXaCDl)D+4B%PS$n307zyxi4%h5CGQvO~G4IaPs#fT_vO}tk{LymXit4DPf>yE{^nqgwtoMEUCBNRT zgYO(FtQ)2S8D%y*c+ZC#DV&FY!Qu>-C=dXEb{tZCtYbTto4ZF#w%;`N zR@`uA9Cj`k-s>|C+W48k;cu|-Cq!tdvtKzj1R+PZEWI6Pqx z4Pi*w-EN35kTT+#-8BqCM=mKh@7Exo?HAr76?L0$aPhu!XK&8Xn+wEaU>UxC>@`X_ zQ#gl>%gH$>$EX!7UMV}--Cx%3{vBqIQ%BSqZVuMIS}n<&DnP|Q@7h*TI1W1pk|^+% zu-99Rkqx$uAj56Z-vwi(i-w_;$y8PiS~xK0@*cE(y6ljb&SN+{>n9|CqEetG(}gr0 z6wlX9)DVfOBZd}Wr%9=PcijSuq^MS}1Hld`!>d~Q`s+1Jkd&Rw58`>g@TjvfLY=SC z4#e;58~X%b^+9_xA8v?wh@xPi9m<^ddy4UK@9p_ zF`2&-LbmJ(o|@vIJG2zuBJ;+cbBA%#6eg?q`+DtvaPp9Ek+}XGLUd`+L~QY=AGe&k zHPFI{A%K0c*b>}{8h-0OJfQ(DuIv)|0MdJ%`@rGzubJff=n?i~@JQPaI+?1Jh8AvO z%-PC+j{M?R8_b#bI0R^aecld(;7|d6u6dx7kweI-XB=ASyXT(&Lf#5LuKCI<$`6Gk zqcygV+I`@${{k==(l#O*{;6}Fdy#0)_z z3cSUp$-)JUV>=$}zDM4m^6r?~GBU7eli36rjZ=~3j>yOP{de{nFd53^A6nhjx@Paq zVc$=r!QYL3iqPe zY*BHw`Uv~kkAY;xAh9d~!%lV3fEErs=IZYDo0lHD`;GL-zDdg6+tJCEN1ox29 z5eC#Ve+EVtoqO1lU_)?^m>k?9;%=;D<5$#(fQ`DF6v&iTm(>33O9Cgxh2cy-EOFFf z`s-vpj|U?4!pU#(FwFRS2GaYuEE^{rOUhk%za}F=xVT>-uOqdz?CXJ}V#yx2)k7Ai z5&AfzkkNOripv#6lpQ8*s3YsqV7TAiuvUP;!XPJuUOSx;A;yzds?N1^?p^LEVv-aV zBEA1HHf&>k9+5o$+|EU|--P&Z>OGyR41^ZE21=gA9W9%_wU%wyLSN$=x841y+@`W! zQpnbXYn7XXIp1UkI%B2Nzy7P&V6`2vz450u3c&lECrb2*ElO@L+nNCI)-5Fp5 z2a@=XtY1x{E+5p^O=A1()$^x6J(rjqg}gL5+Kt!M=!uL4wlHd~YH?-EfO-GrfFB=oH3-%gUZyHZfv1;L++c~-eB(- z2OZ)B5bi@d$%2CG8|&NJ?dkrdgZa`V9ZCYr-DTU8%bTKJpw%+JUI7=hEddbU`sqx~ zl7cOeaT2~v^bNxn`fP0oWA~=kCdWXXCZ-_~a3DzS-H?Sw#`dRVvJY4!O`k)m_o@>| z!B{G&O}BOGTkjN$R0u@x6B?)~r;qg$8F?yZqsA()h-}2dh67_igtqY~M16Xi?`dLif^GY; z>^5vG0>b))3(0tz>oMBNc~5&&HO32-f3XDu^OD`939(`tiB=YgC!Bn;?I+Vt{_$5h z`meoL_YW|?`b?*jv2Ln$R}gQ-F*VEFHuk|3T5cZaEpMVu!dB+(h?wt}FoM!!p_8o$ zefpa;DembccFmBzH1g$U{m=VOXa|nyD`r7j@xryW{OfN$n=q9jqG0{SqG#vq>sd#}U+M;29lE@PNCk?&_C<3zT48scqYPVO=4WEY zFE?dBMJycRsgw~~7$x>6I4!b|V#{gqqOJ`z;It^xlk;!&u~*d16V?tdJ@;viz3LUK z7POa{!hBn)xXEN&AyfYm*0k>$@?GN%a+?G4z-Qnz;2(W@oCpRTH<75cNa0fz^jl zHU}T|{WlMOO|9erE*8dfs!Mjf35SyNtk&! z$R3K6sP0DQU`eVmA{96?iTGM7;?-twEF1VD-O9exAnPpYfMQ_ z>CRhvL}E@=1N101G|*R6wAUsq1xDGD(0{A-fTzC>dQeJa74pO{Tf8tah&7}671kf} zHF(&^spyq3TJAAJFRF^+DXcSzSr&8l>UkWJGorJh#ng;}<7@moYtK%^VRcc49Tko9 zN*6LEZYjWq*x4ZaExx)$KY8>n=sN5CUta5)q^HWM{cT9SnEJHsB}^ofaAQ{?!&we? zVTrNICR&UsB_+`!7JQ0ev-c4ws7wGVpJQ@0%aOSIex^rZPEMiQR|`a=dd^og3vUvs znbN?E=GT{3oY@LC_MODQ#K)nMEWHuf*#j;X^AI5$8$E86obra%AF<65gCkv0f`uBs*t#QyS&2OOhn_+^EQ}DIKrw3 zEwrMn{8>AHiWC^>TGP#AjIYNGqC^mz>$SXC=gzFnA)F6^c<%iAgU=p?9Cs^~TCuXr zOKgAkf9KZ(J67E^!k@_tg0V?)r{r}l})Acs4EJ3h->nXx?mjx&R z5CAucO1g@YD4X5ZG9qnHeJm9b00LlG00M~!P+~GUU;pd}m_PF*^8oWE{UmeGc9&Ry z05{7{qN~#)5OJ4t&prDhae7!05&%FVSvSZ85Z0hxVwkN4F9usELoo}7)O$!%6i~b} z)pXssM5?{c${=?W_33D##DlsPegaY*XaYQJ3~4$CU%6#X8vI+vvDh}OdxILa7O`$8WR2Y#rukk3_N}o`mC{VE zO4lF3USm$aGp*i`NPvAEAft{3%4T3EZ*a6J%@Mk#CINQ->MQO5<<-C)2T5#6I2Ojc zau8;k;_DbpJFP|7VJaMS5=rTJ3Z1Wn4YdwVPD9QBh0GL9S5T|!!av&4#!Ftg1XFs+ zTU)t-0#>hf_H1h#1_#f}pYasgZO!A>VYD4+;H3LZWHTzU_60Z66%59zow_?VM2pQQ zf~2eWPD$@mw@%4vR28Iq(@Qbg3UOsAew6Un)s7&{s`jr8!icjCsz>x~nx9z~<$c0) z6@j$kVJ}A!XkzenwPTB+sSyvj3738sII8usIlj!G#Cc8Akcc~aQ6!m6fHE*>r(AYd z(Rt9~{n3mnF{&XI(&oz$oI2hwClR!F#lsB7h?wdHX~{#Eiy*rRChYs87#p2n_Zy8- zH?$OPo2Hi>)^10v>(+EV=pP1)9E}3*mE=r8g>`9lv4dHChDIH36^!vYF>&VR zdW-%(m#~2Cl_PP}JJ5l+;x6nwjt$?@GzX<{+tr(R{du4hs}*))6HQ~2Fot}qG@C;9 z8-_7#w>XS}>H+Ic)A5<#C)}hg(px>?jFeWvk`dJujkog^f$;+FtG}kuSd_|;V)b53 z-6fGglRXL;NfIz&@dX3g=v%Qs@+|Lc<aK6} zx|@EpIp_tgzQ42GdbaKNntpG?Z~DQroo2iH?Ai8ayStUw|52@j{{wvDZLD4!!^%0G|>hV?#<-Lzi;ui~ILO`Ed=P$Mn?zmw_(8!LBv9L<9a+%SZ?r6DEd|sEP ztx{c@HXH5D{LW0EKjd~``t=OD1FKa!GLog^l1dDny#SQ-EGHd+80l5a5J4YP?yKZO zV4YN}r2ACOl3L18Y=9m_pRJw(lsKx~ZOyBMw=wjhvF0?H{_5Hsxh2Y!C{~A!+bcBN zB&^2N1XjTwoaaBPLd>cN9%;e$$lRr8GMSAx*^V)TgUk!3TpG zqa4_>$Vo}X7a02>t}< z?%ZxFM6b(*xS>r`w_ZtS6VNWSqNH=crG9{}anHB7pAA&-Dh7*rxhtn{q?fx@ko?d& zr3>v`4gdbk7)?kR=0IionuRM4t;InT`8lXb7HmT zA+<|t>Nfimj)rBknd1UA8mZ%afi?{@ZaGAmxmY8Pva*RKQzc`s^BR2g)Va&HiR!vk z(AbjJfKfX03I0cVTQhW4uKtsTjxPR_Nh+nomH?c(8&JtRL5sQR`=%s7;Yd6$VzFeN`?yA5&<20MI1wg;leWFT47F|a}q#=v94T= zJp&Zx2p|uEbQR6UeA|3)vmQ^O8oOGgu5pWT=y7sE{b*XNx8X`Jp!qaKnwKc-7=qXb zHmrW=Flak`!#8hgY1_0dZA7RjuF?lFoy;=q&an+#Zdnl)wYmW{E*3p{q;4R{2bnE;z zPee6~;!>l2caAni!9Tdynxhet1Cp}F1`_D43ZVb;@cA=**gUZQ;b8yOn}f!+P+LqG8mC&xdPD98UJuY=W@nondd%y|vBe^vl}!j~<>`5r+A8@N29WmC2Je_EYVdPJqC=Lp7D-qVE z1JX|Qq;>lPbpYe}MA7^Dd5|RNH0=iz(0iB--<4vYF|`^vK?$&YJ=tK^gD5wYa)?Q< z^ezNmpn2{!rY?@u0-()eofIzshy-8)SJUlZ$ua*cPJ+=nmy) zfRaF%+Jxp9iX-AEWUJ)&gxVmfok2%*OofCd&V7zKgCOhOvBJt-@5z(5Q9_*u&nSp= z7+_;Wuv!0`I&w^9^B|Y&{@=PA9mI8=XHbk}$%Na2 zU^`xD%)$y5wOl}?!C+fZwbd~_`fq6b4Y?yj!40H{S*O&w*@Kva6)vB|6iB=O{;>Q> zNZb_^t)=A2Z~bX#{?ZYLpnAs~FK)spFb@PHz^uS9Gyn$^xA7#FE>zMZm3wEclD*MC zL$Zz9A}aa>4A3!j2SllvVp|og!abmpQZq#?aj+~YgK;b z>d_S)`Kjq_08eY#hJ$GqesV)_bCXb^b5EDB+5;P&V7EKJfZXjb$+eRyU?5-QJ);VD^j}pb2Nhx8-!v=UxPV|A|4?i z6&t5Y|7Rj9KOyAbM6-aBT84A1&CZwM1kTip!z3s#T~Jm_68I35h-)z|PkUh9pC%IJ z%1JhS4w6cpin1;sQk*3ct84khJE5Y79#=iQHd}&f#XZP9fs*&=M+# zTwwMVvNd2CROzVv^kjss>QK2vdOkpVA*|2<$Va#OJYcZ(!K)W`=ZV#8>%oPxhRw=>d+t}Zql0+0K*>GgCtRn01x>P!x?swnNhVbsGEl#E^ZJui4qLx z)pdf*Vf6_mDTP3n){EJT79r1L5G#F#*&Cc^psgQm#@8q*08m9hm=i_V}xz8^qCc|!hRavsj2 z3F>UfHM}37F?OJYX)0Zka73a&0vZ@8d2>9*(|iw8ww6K1uMct1dT0#_(9xinnn9IR zp&-_(?>yb;^>%iiZ8x87RyA*<99O1UA^IS6r6p1wXab00#kvtTNy7k2rL!rjCv4Y5 zI_M2f70-on0Uob0j*Xks)s?WJXm(%m9dk2hC=ZI50AeAKNO7I+&w3E*U@D0J2LN49 zwQp$6G8pz{+&bWzZ;e}57&i~{D!P;}8@S<{)iERgI*142Gp z5)8v&vD_I_E+E=^IFV@AECd@3O-XjZC`8PuR+kh)3J8cj^-ln8AEM_c-o!`0A{LFA za0-|cDdE9MNUT^5!-~|Pbx7r|>%1{9_L1pgOe&7hN0owdM0yuBjx8DDLhw{YQUReG zf}7ww^r~XulN$1!A}n2XO`x(5{B1>ZssH!r(R;##S%CVka~|A^6i#ChQb(oyrkK%e zs789Jet>?i=t@LKlunKBnlM2W5Y_aRW@l*gqq@N!v zFW=i|e-KQzYu>Nlj5QCE_}otQdoeJUtQ6TCS*mRCPkemZ=4@b>6H$ydL( zHT<9NBi8S+9ap`&a*GoiAhkP;JoAwt8>G1OsBN98jsGx>0`?pv0|ty=VCv2n;Y58)!_mLCLxV z9oZWJ$N!AZG?Q0EHH8AsR|4IY%U_~#5oUpb$HBM@mI{vKmYBq7FsQ4U)Zvhgx_iNk z5m{Gj9+XF=q*%40Bc}N_}2| z^921T5sEM~Rwrki|9Zhu3cA;?)#VCVU96j*Q>_#cmv1J>S#!^E0Pz@08@+QDIt^UJn^-}M6Z~o)-!@Hjj zyglzz(Chi1+FRR|HU4s~`rM3Q0AiPQbs|%J)CrZ2X?2uRTS7ThA$?ExaE6V+cuoUB zifq4Bdw^-KQtQ``4x*@1m2F(YPw?S@o(jKR;T_bxaXf5LfrBdT20o1j`p2#o>`tPl zNxD0aKB<5AFF>a3VK^w*2H`L-@j+ZMJd1cm&(n8SS+xT&b;9+)92+nP#c|XN(KL$@ z@c1JaXPAueq;)7g-a4*%4v#&Kt#@$I*n}~)9A_Vq02nbXWR{wtE8L(SRQ5I!%#SJ z1I^~zorojY`l3E0GVbvLk~bVn`pT`C^%%yz!Dq`6WGQ2BflFHY1A~* zp0{gDQ=`%xv5dxz<;5a^zHF|eXc>dZZo$#EI1axr!;py+>`TTrX%{k4hc-lg zmH3Kfln|v-aS%8Qg-5%ovAy;Ga*ladZ?Mw*a?-3n^Xr3;mz&KiGVI2LjeWYW&4Luvtwg9++GUVfyza*8lV)xs-k)`pJ|kvmm{`3UK91qrh} z=U`F#Y#|Zx@{mxu&IwWRA-F;Yeh&$pwK&sJKK&rAa+91W@Wo+Ya}+d_az{_yQkg3c z#9oCigV`6CB(O&E_V%zKZN?gmh-uH8meV!Zhu}GX)EHP!~d_Awzc@<6J&An24P4kRO-q53fn;8k#F;@7Ka^ zcc*{UTbMc_>-sD?Bpo1*$7GH}fvfV395lbn@7SkCCkK$JIN!mCiaTV#hmVrA8&F56 zA3*F|(9eGgK5GTATB{YlA{tW?{TO_qKQP@$Y2FptM9phAtHndCWk7$8`9L;C!dCM( z+ST0Gv){VkSfuW%%;iqWXT;M8bg`g1k;SweN`s~3Km1bPiAaiKx*{bD;u$##-%26o z(0V3C{FjgmdUhGb$d@kC(E0)PF7TkXL`)gddp>ljWS`3~VG9bXyx~&J8s_AIuy6^h z_pR1vvv*-56xlO zW+InfETh^A-}G5fjm^@hk;Ai6CqU!(uYMQMo6B_9u(xKqA@F6>R$_JrYTC{b4H9(# z*closhA16(kwVFZWduBrwdg#Z&%o#bG}rQj`BqB{R5Z2P1La>RRtc(}DeWK}1z6ab zh6|BnU*HPSgA*xAuGUwP`fOAZgd#@*^zenffGIvERQIbnsFwp7gM^~_M zD#I(ml_O=a`IN&lL(IW4$A{_J9Bk(byHS(1aXKRE#Y7}vW478-afD~89B&H3T#So#hk6?3v0EkHD}>|wXKHB z&Fg9%-kz6bY?gMPE83}-Qeqx_uogmkgACrxyftg#w?$ymed%;qB^$BDsEv{F`2a61 zsx5%^3X0o8U0XpJ3qG)9^#3=vwl~uGpY3L|z474xzmLz|`2SsexlSa2JA&=i&qf;L z`TR+0i`SiYlX*`618ST{sgEhK$%z24J@Qy3l)drlwLsjErJPD;Ng!txZM&B8L=nX# zH?^}V6gS;%n%0=l3w=jLZ=`%lZ^TJeAaDI)y$5FhcjrflCV@%B$v7m%*4Tm4EMGcW zY4v3+s>&>_`I=-bU}2EX1?x>-)i5Ol9cU1QnP-boqbOBh*ELGKSocZ=#e)I{(I~I9 z54*QE7B8X@ZA%v;6DI{yPNOiZfob3HucrHpyY#CC+regkF}iYTI0$UiO5tea{cR5M zvRWOC{c!;K6}?YnxpmwPGRETMj`d_cO!#_Zc1=B?7@xR~jRpl-l)w+E0tLz4?e??n z6Py5w{U%!f9AL&B#;WVU_{HX50!T{wl1e3rFw4xrEUFcmgZW~M=8#lq&{BAfRU7B< zJ{IgMM(vj-u0;k)qG3R4E_k8RI01#Un9fo!B*9ylXQ2r!0J&OFkWA3B!Exk{6cVp%w5QK3z*6B(rZXxAGwYE%zZ?WdB<@FQ-_4}1lD!PG- z9n*6yz}(n&yeedtSl|jkLlv)9hmJ8#UK>6)wr;KtA#2Jzhmo-~T@yk#FjeK)e+dNE zm^ZvX_o1X_^*@rIbzj??1b*{pas2nr#^zQ!{(Ex^zI)LB?*4Og{V#(ri_|?g+?tZb zN%b;S1SGefQ`qd5)Tmxbi2|OIFJPgpGMi^3TTC0ljO*a=&HHyBj`!al=Q-(0@}rDNqB( z8A^agwU({7B*c|tQcJ!CC7}D1|A!f3VC{rL6gkJ4;syp}l_fzyh3coeF6c=aD;YO} zIMxxXDOT%c3HFsinr`u4Cz^(EuFETBG#KChopy%*4%KW3a9LZx?g&t*xw z2~7hTPA_q=p@%7xWNOhwPXfq+!g6_&S%3w%CcO*qd_I~;?n?-<^e-V<6>=FlM4m>u z{Gea?VXhGF$|EA1hL3Ew0Zw)GJJ#9=qxE32j@^Y?t#wI2t@n5)3e<0$CF9YzYO|PX zPA>5}`H7)`0T<^|57wv9&1E(@S@4X<&wMPh(g^}&T8Ga}r&b{vbPX*snG+R3qf`Kk zstxX==6%1%??QA0J+TW_j^?^X!Rx^H z>I&}5NQxdpgBbjpr3IEi(#;Kgcko@VR}>nF!$cg!i9)};7!c~H(UBf~5-#jS_tv-| zd3WCJLcqj)N@2b^GgUC^Lt=jOp%7<+iELBT%omOekw|{xV+Isvq0bT#6LnWkyQN)C zh66a1xPO#iz7ARJF=nn0N0Gw6x-@#C8~AUKM2&fTdk~}xRU7-kx9XpDjMoK{2Jhnc z@P`&{PdeuLPKKr}j&9P&qRqq^Nl-l{fie1NJiHF|PU3#)(%mmTe>Oc~Ba@zBa>n={ z(3)Kn2%3~bAEaen9}`qGVQ|A$`Jo)Un=^q73}e~X73$E_rNO%dy$Y9J$s;fpD+YS` zWflzTJXGDDuFm_Z;}YvvCms9O&PNt56=XH%M5*Bo#nE@D#E}xTM^F(U6&8?S!`kQ_Ng;H5 zz7pWykCajN*ved3`+#os-qOG@?w{bk_JdI{#QtX`k!Eul(!Amk?_5uCCR%=Bot=I) zSN4wK|J5i5Wp1o{LbrNnW$1gnwa2hT=pT;7d8KZjZT2BK6M2Q$ zoz=`#?YnzHCKxvJ%9O6mIM3o(> zdd+MMJLkqjGc`<_&5=|QstTshz5o`W?<&b`YH^bqS`;!_y^0UZu+C;Vec{C|5V zTmNr!6M8~D#DCoV=hossG`^IJ_J^YN>(E#=wfT&>f#zc@m>FK8Ha#gVx#*6KhID_@Rm2@Y$S%1=S%CcIE|8R~4;>96Q`;fJ0>x zd6-pAgK=rsO4tY*;E$H^a5UY_g0=^)O3Kw5LDB4(D#DuKt2OIk<|@VmBQBGMcN~s& z?Jqcwca>2z^d=dnivR<%PsN>rC8+Ii@mL`W_b2Z|`$V5=<}!TLZq58tswF0o)CG=4 zsJi>JLa*5iC`d1%xF;VmssZZK;Hr$Tk%uc(6mw#EfdS(v!h84jvdov|f~K7)_^6~JeZE=t&p=iWJ})OP&~X85{^GTY1qJ<2wbA{A0v#}~ z!Rx3I@IsJ}ky9*`tTzSe8@Ato^5Axa6(xk{J?Y>hEKP}k-gn;-+)v<(y4U&$$XNgb zo`IiSBDb1{7vw)?`M=T;zFe$#17Cf${sRB=9=^K1EU=jWZ?<+eGWCDAw>BU6|9yOJ z&i@zTOUcYXdMlgCr^j)>hJ3SU+dJ~#pRvtakS9oBES`x%l)AmZ|8dzwI)_0*_d8X@ z-;URLH4!pUGlV`Fw^>VVN9wx3t#CW)G|0mPr*?5T0Dvrfsry;rdaIbAe_R+ z8o0&=4fVKMg*)G=>2_S9t{Kc#hXEfedP{Ni+$@Qz%6oCXZvPoD0#O zPIOyQ@izyeW?V|gZ>az9SAQlpZI9oK7{h5SOIS)Pv z+A#*lnBX9p)e6>JF-(MkmlGypFheIYdY6X=cX0@k&Frpyg2FU#uF+Nk!wmlcJA=~G zPhTADzkK)h^ysH|$H3M&$)b|Q`L#TpUn87%s##J~MZR*!(f*{5rcGHL+2t_ExqL2L zZ$878Z|CA=qp{E1_FVhvR*z~WD4}%8x6UwOKU}f950@{#2`7@ zPkc@#F8QKjJ=eID4_3La;!|Y*llloS!sLxVy_q3!vHZW$+-hdz|8}eWAphUT=eF#B zIrvg7mj?Ee)xI3dB!avo<`A$xfkTN4SP)qTOP0@Vb^{&MgM|yo!6g#cr{IDr9>j|L z!oK_wb}9G?+<+tOH5*T{%($YTKTpE=vp-dgKmvt_N?w|Gu{r?%xIPYMpGHAlw>6Ds z&=sz`K8A+7GwT+;!3STN=Kvxn+1Ee1)=LJTTU4ID%qb}^P}=+6(?+jTt8*pS>S!=HNOl7SsyN~s(}zWp}{|@hjD~&_Lig}9f?wy0u4(6u!jI_ zv2MU-CWj(9j*M?KA8MH`;!KwB6fQt+1f^@>#7EK=BejBxoQ_@L0e7BfHJVQrzH$wa z6~ks#G`d5S$;Fj_?BTl$@#N=8ywbJ_zF+yqzuWWvFMH;`aW&bt!Qa1g+OiFEQdk0c#J^05*O5E37E0nwlWA%gSD8i=Z z<8Ts=AwUT21{$Gjl;!bhf*purZ>@(*T@y>^_mE769Z;zuZSO4b;ota}F`fwlr_Pxn zH8mcO=LuTAQ20c*kzOCS!Ub?-P`?*_$GUIEJi48Lu6$qIOCxTKy5fK~AVhKh(y~VU^e)QC08+ zdN?WB7-DO!HyThVfm%Q1E@!G_Zm?Bb^&P0{0G$mF1$l+Bj%<69nvPQJ%h;<0G=!rC z2I*HgWt<5FuHPXt-;2|w)$6pV+a4Y4}a}!Gz&htV?%TqkKfv?))a_FY9ZUr zT6)vV-flmw!hg2>!RAK)Y5Qq&BY4)_=s$zrqus5YL4T_|C|p|6A_bfPBs$TGTA9FO zC%-fT2gAy%-|Pv_RhkeG3+@y}{&odLO2sOF71-QnbSA%u*8!^0A!=k@WOS?0g_lur z2v}T0S4-z2f9dt$&^+*XK1!fPwwFM-3wz|@&T!d`%;?zkj*8qHij#bXMh9F;1Ls`q z4#1IK*&8ajhNdDoEcfA3{*ITj?cB;59mp-*&33uV`7#<$=Sfg_BU9Y(lK56Vi)lp# zR++sz&vIOSHU}VurA9Hdu(g(Xi`O_ynQDv=ZZxKlVF zuQ(0nQK7bm9t=u-Px?qo)ScL+^5!(*RfF>X@E3155BmTPP}8SiECB08QStZ8;$dF? z*W%iR;-T@KKr-AFMP|#^xL7w<^uppernZ$P6y)WCYToDSpZdP*0E2&@`ZIqFu{>(m zU{Qd|dEq2C%7iA$C#MG7`?OHIepC?T7j}1jLmcOv88TCeDm~)T)?C@e(GW_O%N)}# z!vZ5kl02~6bpxpQt6nJ$`}Nz;i|9Xhy}tYUuZsS+Hna6#cOLqG+{@>-=)Z+8WevdZ zDH%1ki{dL%4za3syqm67i8Kp4u{*uOVZ0bS{Rnl!f<9E`4($Wb+KbAD{fcQA>KR}f zN>LdGP}mO)5V|TP3rs6<;-CA;N_lv-tVQx4ypnU=q3H}<`H9WB>9j}C5 zMJmXPulTXjN@rM9W`f)*q%;f|j-sP`0mP71jWyW`nvSJKA$Melxlzy^E9q1zy)`jC z*D;pWW-LYQ5KfVavk6AC8a7=&veKNIqk%iKyi&)wd=83$1M3upzvWJ4$69>7G=9;i znEuZJKDXilTulF!dYGmEJ6jL(|Gj)}kNz8cDZ>CnuBW%~D}SDxDcbVJPe^8l+L8Yr zpUvjOxSC}eJ${w5Go_PiH=xqHB=LeFME~Jo{yxMtF^MzHNxYyjY?u}TroYJ}D%L9* zj1+&y;a7?e#qk{bF@1Qs({6cTKdyOL%6U$WI)P?F4u9C!uDV0=hjFF@nJfefNLn=@ z`5(_at`AF|bt1(0+nhqwQn}LgC4s>k85D3nVP0ME3v#cO#CQ~UJ*R4{L>EgJL6nH? zX{}cu8{a^~+sA=*4LdA#+h(V!vBL)-3-sXRpl9I_^A%=PBOQE~(T>*3*4lsz`VM(PTryx;1 zjH0U~sxoikHygZ$+*vi6mzdXzADUoEV0CJ))zzKR`^%n_UYNT8eYI9wfL$EU+soam zt+CN_-$R+mHJ)eJOpUtUKq+RnDpqojTqQ1*NtwGy#4;`9C={?m_?C z+}_UQe{QPJ1OLB|&#m!)gD++EzaN7Uq(&=Zj{V-qvc|lp^YX*N{_#P(;~o6+M%>6) z5{J|=sD3>|!<~83=I2kRWh5AJ{E~U8rGGGhf+7=+i{dYe?~P1ghqYTz2#7! zbK1+&4?aj%-I#VfMvX@lGXiDXu? zmX1k~qQCSUHbZ13+8W6pK`nCoeYCRrkbrq%R}RiE8q7^Ftk<#O%~#P2hyxM<`s+$52$KtGElO2z^bM9x|LDhO9gIe# z+r=?HP=Jm!GoZyeZi}MO9f$l>9DLQ-YtLOM$rl@>QgfKGWy~U{?VST$3K^KjNfBn$ zD!QTpFJS~!$jJOj1yL+B;p14=Q*+|YFo23gV5po~K=2ozOBsTaI;b1QtJvo#!()Pk z=fpI0!v+-sc<+O@K?u~UV1k7S!KiuJL$BsvHrOpt)7ExVr^w^H)pE}KYkZ32|K99k znnc5yKRvrx@6Y@}QdiQ(PU|`v!9w}J)!g35$p1UrTdfEA|2{sqCI2tPmy!VhFJB$) z=T&hjAsMTQFi}ALHz!X(NjdQudr`bN98R!&*#OJO_1T6tpGk8WE2v)jx#(d2z7%MK z$-ul8C@BWLNMVoaf3W|Dze~sXtGBg~%6{bms5Q{={Sn5d&Qp|?YHGoQdMIx4#!Cy6 zI2a8YU>QxKUNlk<2c!YHFTiH&ypNuKohA(~=q1`*Xm_KN{zQr>cZID&V6dTn9E7V0 zW2eB8WUB870M%)!?Tw*qTpU370raM;T%OW*NAlG#n^#pXd1I7$$=H1AaZGa-v>89D>bO=1ag~2ROs*6R{MNATCbKl{R3|pnnZ~<45Q{v4Cvs{$+tVGKBw2w7r9!Y5P8B7rN zDbFHOOh*kegwllx^^3Yt96nKGANvwQF`IKW52&Z=rhJlh;wZA6x9^S*b{8L~x(Kng zO$}p{c5}YDR)c?}0#e}V|9QKa*w-#ZKP&z=0SCWDAOR_LxYD7~c)l&e1VqL~sErV*6hOCoTGQ!Pd@#H)$J z30Cy;6CMKA(a$a-9Jn)(_|5XNNIugU*c$?Ct*0;dU;cRT>h#sSH~WWgPmd1X?|;}o ze)oZ76wwEZ-+TEZymt7Co;Tr;Mb97qczDDyK?@f3!~$NK|(=lP>F9t4vGXpk$!r%PXDQA3d~490@MPdd9yid~N1i_~k>%r?hBf=k)Ot${xY3RU+JM(~3W2;sssuEJp` zNr`?p>!DZs(rgdP>tP=y|{slr}76 z=#eywmJ24bfrYOoz$PQRWkPM-5Sxv5rckfFDX1FfZ2G(JT+rJO$ng}EOv6xLO*yii&7hb>;HLFT7w`0|?xq{9pJ+!H;bJ5FWSmKzQ?cRu2nx_0;xM zHTR&>hOWhyt{*}rm14vinZ~cjwcr{N9X+8N#me4CiaU@Q)BV}W9|KfO$2&uepj3Ti zkx&!{x*lO%z*|sk#=%?%)sSMlF%aYuwTk*I=z%}`Dy5mAaxkNu7oN1?y~YqgAB5>N z5AJk2ajKrAU;-;Q>dK^amrCvl%j@v5YBj!5^0R5?6A9tQ_)Mh`RM)CC`%MLv(Q#DE zAB;E(Jn~Xruga4eNx+@P(=jmiND*r=$SSn48vcw?Am&X4jsjYhhJiLPB9i1;eDlKu zbFU1oFNyR7FkU5(KGy6B?>S5$YyV54Pr(Fg>$aQg!54KRaGztb>A=_TAsuOj(D23n z|E=vz{CB(6+H5}9|L)^+bNk;a_)^XgNUt8I-p$m2Za1@E)Blw)NtS%gwn-K|Z%00+ zEtiM6HXvNPM?3R0$SA%5jcrpXW9L)l55+LXP(~SSPnx*JXh}g{$EsZ^wXVHQW>Wlz zwukKZ`^51sr?{jmHp58VLprX4Ty(&iDMtNq%savjpue?(MOo-y`fs^UW!#ZEnv z$gn7Elucl465_EN-ir%vSv|tYcLk@89vI;iTy>}o1|p(&7L71wpX2AP3fW>7#m?@& zoh2aF!g(x4Ssm@Jo`(^=14_igG@xr5k_#JFW3gncr!89bi9*RbB)8=lHI^Al-XDBWR0D-#Qa?0Tw8RJ6tzK>P8S*1E zf1hIbe$9y!)PLdH^>=XI_2mM^@dTTuBKX|lrT0$ENB9;Fv~UeV~aWvGQ)$b7zV10K^3wRsMRUKq_A3_ zzM^y>b-w;ZY0`9R8P6&mU*>B$$4r<=sw;5%EZtw8dgobnW|k9UB6>~fOhD32pj3zw z0LD}AuX|<*SlB_pvJ1U%kQT`APV>R6%vj(guB0zVjX|eDB{twwg)MIZ(JS}>`jQ#Z z^JYGyIWy9%BUEP3Ctr>}BQ37x!MbvSVdS+vS-cA>wmTb6<-qEcDRw1y<4d}4X3&-B zSP{C?uP%|v&ppkSI4=&&X5x8*!+mT>(TixlsRW zZ*A

p!-ksLq4_cORcy)c?xhODP4g!K1CDcNIL9?lZXPu_J#Fy!7K7ZSyAj;W9PU zQ5X-jHmGz(PAdbIWj@8Rp%xBw5~Ebx^C*#^7A_kZO7|M={(=%0kz_h6&WBy#WSGY> z2AxpY3eFI-;0b?XYyr&P{;zg>^S>ZZrp6%%FWl*#OK5z9wtUq_u@iw&T4!h63MKifkeRtwU7~kp@o2ll!};i z+yS~Wp{gnU8eqS^7zBVYmY{-1*su|0yqMDfu}~j3K%uJV<2V?dQ!TFQ01J0|2jD!%_^Mzus2e0kh(cTxWpwtT&XYPW7F;qX9A8;^hW|q$ z8mw>(eXY*al;>FE-cQ)%&q)gC5FUhTq68ZOKnyl3X5Y=KGBK^#HBMU(&1vaPMug`#u!j`A zsR4cR(R#~5cY4)+&OBy8^`-sBpU7H4JnA~;igX-9d@=4qYX};YjTrjN(w2<_#gu?G zV66>frCQ5=c?t(EG&SSg{s2x?`s?6CBz66@xMzfli{t51$yq^6viR8B;is5N|R+OoGq+UC}Oo z2q-AEGmYX-UGZ3RohUbo7hlvx!Mc5Vq5MissGu5pNwF!5h7tzmDZtiLCp^W+6E*h7 z#r|uxK`J4DWvwmVSY~_b&6mYHT(T?Le$H03e49i+<1>}q&2gU7690VQRPQ!B!kRLq z_F`RMVgMLzTbNJA#CyekF6ghPht!)|wi&m_9&$-7b2%rLzXuWO7H?sZfk|JgCK*qp%3J*Yw(fJesyQY{)%Abs5F{YQjzjAB~L5HlMBIE7SZz!c- z1PQG=C%w)-r=Oze(|nqLA8XFC%b#4wl06om6t)rPQXFKNkXSH_$8x!0YSKM}0PjwL+o^h=YMY>z#o#Gg7nbFhuq3 zD1;~`OJ+!X5R?1?7;E#W5Kdsv^=dv730e(4B@Z&SNMbMjXCG4(tVtBILJYK|0L@VR zg0}5-rOiQW`f9`34%iORsu4okKRKGE=LWWw2T}^}b;wL}jsoImf5!g!aoF#VbTBkc zht(3)Vk9;U94@oa*XDDYx(?}@))3YvAUP@vrkeLTASkPGVM{f5wOW46ZZH5d69lFG zZB8Y67EGXq_=~{Ip5q;s$OD-cjX|JX4<$FktDr~>6gx9p;hHS66{>yGV(v)|cW^x`$6wKf~CU<^#(2WcV7^SV+*`MBmwtrIHivp9~T$ZER2B#aTz$wAA+ZxxD%?)%}bXh z%Tg_Dan^wq@XT+YXCc@Fg)J6K>8A=fHH84Tb|hHrZqbo+6-rdDbpi|+9AF7N>P|(# z6~GbCyXXd43X?Z#fv&3v1wLFCwQ&|R=*+-EMgX%Z*s(A3Y?)VN=PZIBg8Ue!Ipzs8 zf&=d{R@@|739LEe(s*hd8?`|4Czs-T*mlJj#Ncs@amMP9s!QQWo3>oSBt>eQb*F&! zyP!mf#Ri@ll<2k0g>qp^Sb-2>o6jw|A_L)U?I*U=X$fb~0+AlPnw2f}Z)~21JU;r@ zyFu=YmL*aD{Lz{&u+A}n$lkQ)z}jkR(Jn74#)jh8YMK4Gh+z@!)R+N27|g&>UGH0qZgopXIh-Lo;5Y9@?>sJ+@ewc*8o&J$Jan#Rl#hU zAxpMFP0jVuAxN~elxmPUO2;b#fU=iircaCZnXLjm?rW=Ru8+csuCYrv`Et8-|CK?! z^dj~13k*s685u@kq9IW&GABhj-PF|`N&FPPW`Y10L%YaIpRdU3vD6bO(>y70d8T;y z=o!_E(X?W%3HM-A3BV^9M%^m7W2teCI=i>$*eKH&m$x1@dCF0wgv?4F2IQ)|1e57J zKC_I4l}o*(-5fWRv&0$+aGB@aRb1K0=L`FXJp`m)Gaxk=nvy7PwIree^1)C(3j3~t zuVumMsOWo*xW3Im>vYCGYN)o@{3sV6$wR3nfH_*t3v~rvuyq(2(hfx`wNPML{|%LO zYwH!CtFM&4Zh_J;ZZ zgBBRC(?PMI(|OE1G1dhqhFD9)et_X7NRx?)eDnU@hvWUX$5<1J!XT+tNCuE-rc;3S zISk%{k&K3+(s*uA*Ujk{>CDO|EOl7>ysnhpN*-JiIicZV%2xsKpeA*?ATm5IxrH6w z+Qs*b8-l%@gE?Fwr}kE-=5=P$fZcwXZ1J5q=uH(9{?zJNE(`SK@Ih$!P=I#85uSA_ zVGr=sVt_vz&T+e`3h8EGoF3v9VumG4(*6P!apwR%WKQc#26J{HK+e~2Vpk$nqdKkZsm?g^otl8#T(SWqov z+>%kK{U683VtBuuy`kQswtflM*}qKXHS7C^#arV!re+N0Bg`IwJFIECI74Cwt7xr| zjxY&mnKB{-#G=*IPosP{00MyQU_2is;S|~s!X;ZtZz@nKMBFU_@0>t#Q`1%#?X+s< zX|&f+aoDJWf&{7mz6#az2WkFVpRD|^xwIq0ZC|VMH+>e%|ILk!t#tkG?e_MA{C_W> zo6G+T@ug(G=Sn&RbSNi`eLeOIh}AP6NsrRx9dEZ6JJu{i&`-P&qr?*CT1{c!*9 z<8vqV|24jpy#7a+1(sVu>tHkrr%BlJUe0Idf%hsLh7kO5q(zhc(J-2Y$=TSe;I!Zb z;%Hl)u1SS`MY@R7+Bp@Jr2tB!TSH-XSHC7jnOlNVO=7Y>ehMyPD$hu&rqQyj7F@VG zR(6BQe5~X$49Yr~%_6&V=i3OsG2-_qC(q_T#G zQBbbt+LICZSDljEM%`%Uqm?Ip96u)I>PGYaviiK|dGnTW;Mmz_cvZ!S!^M*I#jSMU zi%!+;0;iXNVfVB>%06XPQ^%J06KeV(6k|?2-ZS~Ijsw=-hpHF z?*P4s5aC##Pr|>=6`mS9rI>UmAIGXq`&-bXfUz-9Br>?^G~l?y$}-tMS*06khY(mP z{Lz4{QKmgc=iGgd^Ap-T0HyZR1(q>_vUdu~`KcJ0H~=1LAr;USVl3$!GLi#PnmZ19 z*uIwvw^Y zEO5XMDGdPQUJ#y>&DE|4tT@gP`-*6sBeU;VjTZp`KZmg%4?q(^4$EX%)qtPqS{0W` zbda1KAu_+A;h<*M6&RnwjP37L)SH`vlCU>(x7h5Ve$XgXrz8QfPkj!$8^6U3$evPb zz}^~Rw5~+&^Oj=foA`S@NY3hP434q4#ZarL)jzrbG7dGLA^VojAHz;LSY(9HhT^yU>aj2CHEsYEn zb>H4fQ*dqHd!GAs?0(JWLHA3wElmY6Gb>KP&9qoBgb2I0^L-pra`8Als>v%3W2sXh ztrZ*2!vzf&;e#C)shJ|Uu=T3A(PB08LcHLG&Ab=R;ROI#rCBR_aa+CUzIo;RB_S#X z!;32OSxh_kgf~|_;q&-UeVXSh9cQ^ifeMB>N0Qq{pkm_xYNf=<-%Ct-5Q`qfqOV>o zN(ltBTa*TDvZ)<0JOMNX&(nwZva})7kihdCwVK|{Xw%$3`YG}oI`=8!g@|9WLi=rp^ypn51Pe7(%X{wGVaH- z+}5) zAPXH;mq2?0%v{-MwyL{c)BCIX`>&pk<(YW@-~a2iAxV>1HVT3KLR-CX?)~ro^}oE! z_Ku=DPp=x@n<#c_16Z;O#zjc{2N}u3z{GAS(Sm?tL0<|U#C2WRL`!p)7*|yD<8a_z zMDv*^7I~Q}wuDbRT##D%#xk8Q{4t-x5g^2BAg}c1BU&)DpXZ0|d2!jzFRvGW4qoi< zAG~^bu)Y6!^XYzb{` zJP&)+l+|x!_Kk69X7kpW&`Q@^`ds5AP&A~yv)+D+xq3X^IaU6IUWh4lii2EW0vJCB zqmelp&{w6$xuT}u12bF#p{-Y2JKG048(Ta3n_JD+t5>fNp0!_ZZf`$ry=XW0Gx=pJ zYg|VeWlL6i4w+kO@85mr(LbNwc(Pxd$x?XgYSp|ORyGT#{qq`( zERwDYV5nA+RYA-NRO`PuXKm$e=kHpEku?2Wr_pUnB-iJ5*ONGIpNZX)1d=`aTa!Vo zTp`WHNTKssqf_K_7Equb&@!L3=X6wE)SvePX`-i_A9D+DGB@d1Xe42Ip_$29<8$~a zT>mXrTJLPyTi<_w_^mXJHtnN3+Q1E6sq3aIwcT_aWQ5kUR-S%zDOgv4FZpV%qp=pA zd`|X|A{QHNDc8nUKgFg<A|PHiY9VWGX>Rf|s_cV1zT_gKZbvi6@} zTF(kjclSRh8|^h}X1b)^#pP~n)pNNe1eg5^rFXwFoDD# z@l=mvJIhZNkQ4tvG#T~Dnt~NP*tHgX36=567=?K@V(iOhk~uz=2FHEZZ)D};d; zL$@jJ9K4ZoQaVPLE(A29sSo^~NN;Ks_iZJ=h?VYVIx72I@L0UxAbafd%m=F-pg+`j zmC?|g2PtxtDGOL+mJ3CB1AoGPbOjagfg_TTn2Pp3=V-~TN`S|g24SKXJ`o5toJKAN zBly81uI-^94xrRO6(NRq3Xd5bU3!?vdF*}fX&LX7&Sasej=9Y~%*|%jsUMOX8HUEz zN%LtRO^0kho|EMm$T3hYOV(C{G(4bH*&J35SD4KMsfnu|#;9u~jUuMwl+t`eJqT52 z{n*s+eWI&@bB1(L6*5IdjFAR_E;UqOhKvnHttH1dq%FHSrG&Z>bSb&CkDNsHrbz+E$T=Z4p}=q}gPhrrDH2-%_9ZZ;VJd0p+E6V4IjCxu z1Q`G>036R-0(SWz2(@iL1HDX5^azV@$0awJ%2rJ6G_w&sdikw5JsMn;D8DPxLO{B0HmH}Z)2{0yNQXMRG-6`CZ3~h>Sqw0=KVB*L?DbWYp&#@2{QfD;F z8o(8^u{KnKu`FE90n1Zj&Ula(?0`t}omw$_pZ6%~^bn$smB2?F&^7Tc3ABaf7X2iP znS!nMpLUW^aN2t`HtXBJ{`X|6bqdLat7 zDAIJ)Xq|@VAb|Huy5mtE0$g!?PKs1mm{S0}WOngGkORPymwO@XXd`f3fdEMlsq;@eq`3;o# z`^P`L+yhy2M8sW)x?|-ofhTgBf>?4wQT<>jNuv1iCG7sH#!|l5Q$SM;oU&VVA{&sY zCD%X(f4ToNRQlc}=~u%T`@SjO-^26ms{cZ;Zm6ri#26{jqef3`p4RO;U@2pw68;Wo6+WF=}-31WuVV+CAm4y^Wt(?O&ij3E$6in=JX!bB z%BiDlWK`r!2X3dKI+xi$p?2UX2N@vcqLZ*x2c5(ZYikPH?!HV6L=^?m29Lpza+k16 z)CByuwcR8%s#cdeU_@=;BOz@;P5FCi3oNuRgtiUkRUpY@7XkCGwFG^Yud}&QDuY8U zZn8&5eF(_3QhJ{(u!`Q%~E&dTqFcThro$YB2U^adRob`7+S@p*bLZZmsrr+cZpw-S|Q|*h@!+D2{Dvk%Mr!<*Ei(NY^>}?V{<{Q zEGQ+nLre_VGsq^-^y>Ec#LmFTcNA9+p$+me29V!}E9K zjW>WUcEW3I2@{~S6CRMlta3#sJYMS6o$%%$`9aGG4~R^_OWXQRKDrKde)qj-oSYWz zBr8!#YlV!gA5KT}*lQU{346IV?cr$qx|y>hn8ktB_&3M#ua)dm9C*3sU(bUUy_Kv( z^j^k9r^dJ)2R?YmkAg(YX*h67Q(5X;1`o?O#(08bup*Mq)Kz=u)1`JcX$OgQk@V3R zvjeHy&}|rc<$gx4q0Sk=IvRO=8tJk4mU&IMLI8d!_#TWbbE3049_0joh5!S5&FQv8 z@X8O%gAa+?bPT##PRm>&O6l(%6c4J3TbqP%vPm=si%-VMPA+=$l!J) z!5Cn@XV#{-aH{QBijtAY(!!qHUi=vX#4K}Mq6d;#dv65IsUZkIoTR5sOrL697h)#4 zwfv@xT!|wk!9Xc(EFBSGsQqJH0K7YmrjM;w+_EoZD^VW=-PlM{sUF!xrAQjP^g*k1 z#xWf?U*Xo|sH}sir#n9DCf8~U&e1xTmfmw-gcCL|sRMRq&Fds5s10w4Ps*C=M343{ zH5J3?jRS7Y6cXJ{{WBU#&8;WjHTOfY7dMU>12>N8Zi=ta{DXDHs6B7kJ#Om#I<&`q%WJk)7F=N=ej`K5$9nH2_z zVt}m_4eMSIM11rOL^IaV;8g!MS9roU{YZ(-{^Y_NDuOvs$ONs|&x28f%44j49`(IX zL4fUP#vzIbrtpH7{gP-+4fR!_B zr$Y83{$6HPe=6|<9Xd?s;6#Qqs2_s56gI+S#sCoK#8fsS_ozu_j5V>wphUyo)$aq+ z_I>W-S%8s*Ib}(TQD15H#rT*GFfrB@M}@Wm(#>cXo?a@bUzPuR*h|=rX7s&Ad)+1g~SB15(1y8^()$j|RP()!C#nS)@ z74CX_E3aa4tt(7grpeJIs&pmnJK0EEWwW9@ps&I3mB3Q>%pui<2))zOT|8tiZq+75&|}`Mg~;PBM8(yISKPG>r9-= zhkOcD8-o6AN*unSP)V%dmgdfa>O=iBrlGuWUXGpXZBa zSe_axRH#*@L`qC?0*yYJRov7_HQcGqf%*Lke!nHHLEp2LZ0*? zbj5B|w`LqNjOjf8sV>xiUUg`ti2Lbk@`_{vhB+H6z+VMuqggFNQ-hVxh0D{g@k*CH z^*U3;cZ1AO{*B$w z2Fi2M)^}wgsIr-?bbALmwYoG9ISKn8Pk4^S!J7pTf8i&+vulDq<`y<#KW3YNPHe@C zNTz^g2Y@TcSe@lK;#yW0QuCRl5jr9LC!Cb^2cDSzTTU(o&O8kcA1OS}*y?qxR>ObU z$0`q!VWuBTo0PGlB(UK%JKjFC!!A7Xg3nm#ijZ!oA4)I69trSAOC+HOk>%fV~M^{^{!E{ym2t;KswKbNIZ>AjYvR7ED< zjBu!l%8C563VDHszQ;3v5)XpeYbDec9%AY_Qz%0=2qJN|(peH^i)DoU?05bc!Q~ob zLSfv|aD`2FCtZ7I#l$~1x9u8>sS%9!1ukRSF+hUfe zq0G*uWb(?+CvG;zP_U!hoFdMFyH9lBfMipTyCa20ICg~jE+66$GL#pz~cFO&jZ zp#-e&0{g_%vTtVn+AQMUN$bheZlENg;24fvAXG8D+gy}WKPJ>nqRBq^dMZll&-BEg7f` zdHbR+@a~6q?~ZqaSr6^Len0Aci!y3mJMiKYB&k9FBlfUm%jsq2nq{=547DSr>N>>k z)+r7y<@U+WSIy^3&?BR?VW-)#tsFTwuumEsKar=!rHB4nNnNf?uvBq0fXdYw4g2u+ zSS`%2JwOwC8rJw=?vpDJQXA~IOH6S=1q79#zB*Bb&()*iQBuU!ypm65$!@ajq;u%3 zb9c)T+ts8=EQdsm$qT#*wUj#{V}q6P?%x#q;)1NNda@cuPeeWH8qcz1z7bXp8-%b7 zo45i9uoC%&uJpmggjWGwJH0_T3P2W(eRY-K6^th724eSxa1R-=$JSz9)g;%-D^I`1 z;&51!twswkbkop|&{5N&MPPJDSMLQW>vV?2AJhrTLQL>o@z~Ale>N44HfdEd58hkH zXy5!okEM2x`fQ#n;RV;eO0a#r+H7no8BqO79q3>V?#)@iA%&)Y@g?}9h`;NO-mc9) zK$&0?(L+U6XmL%s4lH$X)QT7KF)*-`U2vJD{UsJGE2G#^AdeqI+!4A{#$T6u_myj7 zV|83vDd;QHwP2C=Cw$mzxE*b)6vtewjieWKL!G2}xVzU(;51kD-Jr7u-8RgIO1N(R zAc_fqgQTLbRqeR$)l83bi|WhW-};*?9^qg9l2$xyLIBD{iWsT${%AFe2RPG?uJ)W`DM&@8WN{lN&6?cx){O*f$HkPt1kqj0QF`^HBet+{oc=k!U8|07$~g6ItzWv2qfCj>}TnawccE(mFCY57Rm44R` zxzIi~n(%vPIm1Cm#%h+!{fgavNMgk2L5YMEHQeDOn$&?C)XX?=d+7Pj=F2 zU+;cK$-Ni0%78b`D8g3_pBx;1Q zq-0exEesMhM%wJ7-l;Y+cGnD3&e+aUHM%w-X>qD3QIJ=A9M&ek1DDF=a%C3KP>d<) zVfbWrIQUSvuZrPvsUg(^zo`L`?OFZy#U4B=J$SYFr9Y7PUl--}2ish|^*TD}w z&mFPwt$g>hy8D^kXv1CB8EVT}^uM{Nz0f4`hq%Reg!7KMGY9cACLx3RnR89u1?Tsh zT?}WH54Yp?Zb!y$Bczx+N*KJG&S%-olHAPlERJJKjC8g@%dW%&Pno9*Vtb}Ik7-Q3>Vc*y_0kI!xAe=o+DQYqmL zJBd73tD*o1GcCXX)kM;$FI@taZ-19`lTt#ZX^E1Fv|kcnFFA5$ zV(fJW%~8m3h~(k( zyL^`Mf2wxDfBlFwb>&(xi}`wp}3k>X-zo?2|+n8Qteo=7>B9weeEkW}@{NYis(R&&QmG|iJhc+tnUS`4~x>|Hvb zgZ+9?Z=8~T1&efY8eraqzh?g$!aSW z^n}&FIedG3@Zt2syPpocJ?~S{>-nFcw)>j(g9rjeW4z!0`6zb`>jxdEn-(j703gO* z=heaM{hxn2KHYz%X2w@KHLqi>f>S0t)}S3p;js}yM$f~G6{?z61&|f|!!27jpMFqh zTgk3DW!1M$^5!*bQ<>*;9=A4}`*_)0RPa=@J4|BgY74SPWH49mzeW}u9ss#9+>m?c zbPMGD`$_a%>g_r6<0VID3D=pMylZXTA>tAK=>)M;YU_O@H&S#fdRd)Q%{hVJG12d9 zI~oGTbyCZ4!Z?r7F1m6;$0=*`QvR%;B9pnbt7ucz0z|+GD=VBxG2tqEB971!(VtFi zJFjUW+<%@-Zx{0O$-Tp#r~F-^&uehtuh;c^mHO68xlU=?$}*{{YC+d7>WN=x4ErmW z=l(NWwJO@Kn7U5YTef!*JReL{MW2=Y|F&axg*le^|67|I8UO!w`@#NmFP}U0|J(Rd z(&n?Q>x*k{DcgnRT5b>%Z5(IXo1{GKf7tF73mZ?!CevLfR$^rjr1z`-tnU9mQpY~J zScL^FrvICpIsX66*3JX{zmLz|`~MBT+;a4{_VA;BXoay^En$!pK1>43*FN-*Cs|QJ$BM_GvYW)WG6ou$9tRhIuAP~T+ zTHZJuo+Vh-PE6jtXhtbPeJosveI4JvKCW?3QVj03Taw~_Ct^3Y;v0B8%wrDw+{`rb zh7tE<)XB4r#f*=an+Z|RX|rXT)kp^^Yr_RL-D5l%u2W#Dfrq>c-y15XGoka!W0Tel zrk+CqV=U8v)*25g9xT*V57i;{&~U2oh3xh^MLqZu#z}e)80Vo*p{*DkMD91(%5a*_mcETiyApLM!qU1Sv1~ zSHfnA+q+&_;uOfSKv;2cu+oR%RQV}a!s;4Wx;j9!`6^Duqg*7cUjqqs3dceq+)*yf zl9EJ=p_7+*;#PdQZEUMA#Q(se(XH-4X+Rm0Tvpa>92^22}Lwz zucd3Rl9wUWkJ%2+x=ZPMeJD+mAYG&SB^{{l;w*#nvMp+Vh(+$^QwdFDmp>LM=wUn5 zC*EgVSp*YMl1B{B#Q}!~ju;XSjD?!5d;2k3ySS%D9XXvk7N{_0t&Eu;RxHB`onG_H zfIm1A(=RdW=xDxEjH7wI|MKAU@Rj=Gm;Ij(Ux9e8UoWuwe30JIvv~l3G7W=;cos4C z+G3hZ&!_uxI%tKsLFs@`CA-Lt<+%k$pXmpi$N9_S zVur3^0I{Hux!Z835v8n0Ppvs~qEDRLBDdtb?@FLhVTel6S^HSm6l~m72#JjASgsOl zOjtJWm`S0Gqvuk4_2Za21i?nd;0na|@ywE;#llLV=oGjvqd}H7SiX_Op{#CCfbEd1 zuy9_NtAf1I0U2>tbtO&-s4S8sy5Iu9j>L2@MX>LKGI zytGU_=1{F3-h!$5p;8X!7Ix@XXsk~jF&I%HPB}V~$XP0UI?cUNH@!``k=5@^ev!nyeHPv+Ag7~|jrN)>rtclxp2BOU=%-Thy#E6s2&#(V ze3raxD=;;sg9N0*5bqSz8O^Upp87L?tkZ0c$qGh6sRK9jN^5TDe9zb3Qp4JYk7Gwr z)L!=)rf7jkgg$sc0<}`mee_rsfMD;=q9A;L*B)yBuC+K}A&IXidEr zhtloG@q8?9e3=*4;)f_XxtfTkeiGT085CC64WN{O0x7N*TP7xFQ6F{92@^?{kEXLg zmk~!wrsNjx1t24$3;WVVbxn1Qf}(fdBhze{e*}e9^oy=i8G%~#(#2%TagYX+Uepiz z?*XY|sA=B~8}!|;YQINZTBV|G5NW!rtFPqA!|=iBp)>?Ujn~}C$&65iX=z&xsL3Dp zfZbdw9*iyF@+0E6VWI4@*!?8C&;jKIPN+8&Xa|Ga(hizRL`5crSc%(@ zMw|e}l1r&e zWXVg}x14zkqp25=fwJenNJ3fZxNcLx$nyW`Ec$YR6#Uutj{NsMe0ewVze@kl)PLV@ zZ9de0zn9O=`Tt^kDVg)9pSN?pWw4*8LnyABCk43G=Ug|&oL)mWIxf>7uYFbS1*^>=0y znG@pdIWVZ;lj*hDpz=DWqi7iRGR)y{pnihnr4A{;lVChe)DqEDg7JfjE5Yv=S)$LI z1U~3jD&x{wV$61OF(ub9^Ds8k%aoR=Fd9)MIxe6X%9ed@B47@JMONDQDEtKJqDOK` ztm<>cW$6UXXu&C$!zuNyoB9`4p+ERQRLv}@oF2Y;|I@*ngSW@~$A|CUo*w=5?ieDA zh2^uav|>qh3iaTS8-y3`*{ms9t?sp)Ds_Cm$IYlcTdh_)|G%}p z*?g$~e=naq;{O)D6iEPWR|4n;zQV!v!x#A9+_EoZX<(^)!4qs|KN>~3g?NcWXH4`u z=|o**H1%S@F4oqEX6H!0=hHrL3KKAnVLhv}Y%f-8(?x5u;eMN3od(z3Qz#mZccZ`8 znV!b=`6TuSL7kf02m=G&v%vk3<<9HeOP#1I(fK-r$DApB1K3lF;LoDbsH^D3nitQz zI508d))I5Uhj%~y^kVH=lO5y3aP7+x=GC-&Pc&wK*8{pS3nOiWGn5tN>jj_@rY|a=C~OZ&(c^DIS;x z42c4v4o5}PGRl@L1QxP+N2dg4WkulU@ofdvjqT>sXPbU&qwBZ)jdrWKy}i?W+J3tE ztljTEd)n{$8$r9dv)$`$>}+iL&)Q9YqvvlmxB44T+ud%j0H_Cm{6U}Es-ttmWxqzC zPPw1x?l8{v4LhCmgD)8;UQq$yoAdu1zFZXM#v|o0dz+-F0htEDpSf~WwSi&Ii}Sfb zF@S&{h*CAck<1N1vW)mUDl8-c6@gRRlrh!#?k{p_x&dEI3!E~gsI6m1@-m&TZy(ii~6QDwXoY`r4vEdsU{Y!?ApBbi$r zI(16f7Y(d{@0bfz^yIZ5z?zmxR>g|asK2E3sp~^W#D1fC6hUi2Q48xQh_6z6>IE;6 z)p<_!0Sn6JcosnyLaJZ_38?zsYl5d(Pmm7Xl&c{deZ_nix0u~gx_-vO;ud8NOWGb; zA;sR$Kfgah4KlFPF}{0sd@#&_KoP84aTA-qCx< zB6~}MHX@mM3d9L43F+@>7WjQ|Zvlo3Vb)wK{DSf`;NRLdAp;((@j&q1OVe%ZrHU)y zrC{?-YAB-TIjIuIvv4@1g2J>H>I_`c2H>7O58Ppy@#oyB{7Yq+ zKhus~eWSVx;easBXBeU+>dIO*v%J^g1g&XUVOo-5b3_TaqoZhOTMM0`N|O4;g@p>V z259b)ieJB`mVjg3k8(iGCbUk_-P&licebBy`diyiyBn>Y4L?u{!n46srC_zTgZAb| zu)WdU4xR=q6c?Ke&_`OOqBhu64 zjNyL|W|2HwR+i7egG6|odU0nGM2t9l3B?^N`GlZK(pvw_FP!M;}*21Hj zjimoSd++|;wvi-^-k;~MK)mOSsf2o4R-%k%R<@Ol?iWASmXo{3$Dat1kc2f!Z~@SY z;_?4}yQ;eS-2f<2ijxt~?!+SdSzTRS?^1+GosIKjWY@=rVE_q{-1J**A|cI~8LkQ? zhseRO3Q6YVC4w;@2ILg&iYxjb+T@E=a}Qa;FUi(&=+%?MgQunvBi%mRNJqPmvYkg; zf>xcLjlSI&ZD(hXwjVv-+5L7K6c_34R+^<*w)5TD+2i5q_RhCk;(s=`w?{i0T?rQL zsZDu7AdjjrXplun6ZL4IO*G@OiIV~4g{c!m$|g?o+ZO61cVpZ!`etzrgzV>%DSeD_ zjROxHWc`d;r0Ut2ZOyeEt=KG|UJI|IO%bBHcch~J!4Vmy1$L7E<ARK;lv+% zFL`=CEh>nChOjlG%Z`HvFnxD)HIz(W^Z<0@Y0gj=0S0 zOmF}&0A#*7pUX64Y||DG$$YZ*n?wKr@EmR5>-G9Yak*L-s|r@DSvlO=Sf$Obs>N07 zTU(Fc{j<14fS9Q`U9eQ|d8xl-lxqL102{YhCvI81pAl@tWsgA;+OzVxL1H#^auWgv zr0gJO8K62q#n~VMK}mpOUS{&_R4~2t9utQ>M%^i->YoBXL7WqaK{`Kz=Wl1E5UON; zjI9l<0Xu^vVDtlT_dqdZkPj2Z*%>$_q+`j#z+y5f#7I9gM4(SxtnP=hm=KB>X^^bB zpg9KrlaNN79W+b9=KymdL5*8khc@{iK^bAVQ*cfbz?(`Z?kdftUqrQ*yhKHxeOVNr zC}ahd9SPPhqbI9OcE_v@IcyC)7CA}{CgcoPD!Bl|N$L&zq%VZbX+n$~rR6C3G|J!# zR@nh>Nmk~|r|$P=QdL>e;WW#p@+M+!$2v!sK)mPKc)a>yT3iAZsOBISl>$O&I{-y2 z9x0D4K%#up#Z5@@5HXa_@uA$>unPE6d;TxeJE!h~5gg&yT{un0xBw@{!syd5j4_Vh ziX4e^rZiM|kmYezi)lKM0;c;*bu~FH#?iP9nF|gD9Wrozg0bXDQfDtN@z^NIOd=8# zErooF6LG^o1DrR%6?a>pE@p55>ZhtqQwK9UkM0P`!({vocn6LPK|gXYTh9Twhh7#E z=_|E*cog!$+V#3d`dZrv73|RH)nSQpZ{{NV;$5Ai z`5}e#Eg=v^tfJD~ff8xOOtIKz9Cvw};+xSG^7F|l#Tzres?@0GSbrS>t#A6$mjMlShAE=_A&r8F`+9H~!vP_luXuhf-ydPeI?J z_(i>+o#=pEtklR`V6sq~DX+vdoG$i>$1@;!o#Q zYJf)8s2cdGXm7k=e%w_ISa;yM2hJ^9qWJ}*n9uSI67bov{H85vO?L>^FSf{skb}m8 z535zl`Pf-=jaLSvT1wxk&f=9jZqFKVqDD7s9{0^Tym~ENa;v#uS6oyKTT}zJO~GJd zA~X4?V1Hq8OF$mY(Y?bt-oKM$cgG@wk6i?b!w8K0{Z9qbSm@8$meg- z1C{3qPs+k1LiJvhKt#!lCA?wG0J-_VNVU~?5eKw{g zP5_xQ4Z2mE_)hXB?Yrr%W7WT4HK6WL@M0H8o5`t+-K+M@(7iAPx6Cp6DXP#|fmmt%hZON~*#(x_ko?b5|7JQ_D2a zdeF=KOm2hD4Z1d&H?Mzt&wZIVNMAij5JaP8Yv*f*y`=ozXQA+ftGz!1uz zJ5djJ8K9ZA=3TW4rV=6Wdn*`Nb$(Stbg)?ra3|@RK$z|jrgRE((q1@I&lJ|KJ!b4s*m*9y2^Q1B0(m~0buMPG2HJ%*SI4gQNsIW;f@z zE9Q5&sO)?mxmGaG^{$jTnp$80H_TG+PF-mmFI6I^Kr0(5yG|WI%jT{&Vp?zKv#hGi z;>t%*dOI~nmf2`Nl$@Tz$?M}|UHlH%Xf-$9DfelTFla0|eFsQoTD@evBx{Rdr`U<+9qwYke%^hJ_^Zu<Sl{hO?R~1aF*UK*a3gIucR^UC(VKxT>yxBQ}18veudq-c0L zJ6Q?n5#f7C5Wqy3Hou3!6RX;=3Z221`#4nh4DF&zG1ZYa_@wz3A+Ns9EaKJQ#l0EN zqhWpK)6Q|Up;cVh5~-XxeQ*;t+rbOp@RRqqecE{FEa*ARtip3t=}@k6X|L2`^#2F& zPl3?lg`R+B`u}))$BqBr+IX~kpa1<%KDSH%Eqv+t1knHKYWJ7}V0&}tu`d)xUyE?a zzZ@sQRVCvh6_iD-7=w|yT4z4(nAIJ|dk2Ni;U6k^{Q#aMOfql=Q~e}n`YcPb_hs5H zm2Lh^Ek@OpmHzq&SQu_Bq{Q)B^5jZ|x#C(V4gfsI`hu6PDR|8;M+45Jl0IP~0i5iI zEK?Z);;HAO52iX(^=4AoGC%--`-!YeZAIi>A0EGb{o>^3gP-0Wdd1_^U{8v&EG`A% zE2cj%ER_|i=_#6|zvh$qgsH7|0Zi47K>!25D_!t^&Y$yC2J zF|`npLXXy7veB6b5co)G2RPl@s6d%qUpfbsb(p1-bOVeE8|WD+*(b9i9{`f0vSrkM z`a%1lz(VxR(s?q(v=lU&mRIp}muH6gIrK2*NmCDrT=KkaJfq%jKLbNxV+vA|bsY+H z&#GC0_5x$-2(s{fwh^U4=wCdBj5wJv1~KcRNWxxnSr(WkLNqs-7x9c#`8Wd#bD2{9 z4YHcdr|4YpF)eevli&+f%xg|NmNWeZIs$oh*R*ZJF)&NWHY2J`iUtCM0o>$;fQymK zMcSPxL9}ukQ>vIU8)mtH-T;Vs!K5IUCNJ&dtJ2JP)a&w{Z-QFevAIxhBWuYUN~mB#iN^{9%Ow4Fx}HUEZ}Jj^tbv8Y10&4atD}DXdrti@ ziO0cIAk}FuKr2(l3GNC@y3#W;gL+(w05Ir0iZKAE_W{SS8?+5lJ6H3e;FPMf`M8Nt z;FF^6OAyv-{*Vn3NK3?w=qAPkwI^;K{zuvv7Y=^oDFB!EHLa60&Qs-C0$h(b;%b>q zs%B{u732+c>rJZjHFof|=Is;TMK#tSQ?|*@_*o(?8p$Qb<;kK_Aeb4ln@TpYVH3d@ z(#{g)2+(z{wpJ;RsQOTu2_&;Cq&va8(Umo5XvIIoI?O>$7fb(eY2D3HT7Om*?u|Z+ z$^Vc~Y}xkT&Bu?t`tKXtkQ4A;{=fat?aKcqzAVH3%Oz0O&4E2tG3s%N2|(BKnQY9A zfpng!`Vm(u9O`W)2pi^cO)3Ez!JiiR&R&3?0Fu>b)U~EGxq=4eW0Qe{BB4OpWqpdT zlUXK)5c0nm5c136tchTO%5%XMUnP}Tsu&#RJ1ev7H*A0fNv$=@3<6c6cvc?)^{(U&6SGR&@ymVm?hv8 z&X4U^v^%ZnWti_dXA7F4F$3el9a$cm=|ARN7`|9fk5 zXZzm%cPF3a(*JgRS?Tl$H2D#^ju znF8NyW}Y>YzlwL|lePcPs9+#}U|QcWou#<^Tr^%~?^DcpD$ZzB4Ci(|2+I4~PloJ0&vCn`rsZDV z#D3&A%E#wFAJ#tn7Y7$C9~WF<4=0hsSs5x&T$OA#CFHxmO|9)^aNj>O=J7DUL`5AZ)viJTu!1@$bOdbK*Fc6goA z>pnjzDXOt#US@f@Vy+XUt1RL4Fh4ofn3J86(F}Bk9M5$z3rQah@00~Ne5c06@B>Ia zle~g+fItqFbeR&HOF>3wt&seuN47IW{ZWi}3%4TD57`5aLj$u+ccP=spAG|#T z40P6oHJ~*ZGCy+wCY!6MA{De@OD+yP#+<7(IJpwj*q9tz4KMcxDtk&#Dt$jg>7o7G zWBL6?@U(9~$$dxgb8NDtkX0tx=SMG&4_`wyN5>#*{J?TaY>Zb2Z{GyRFn);IZ+`^v z30~pZ;SUFIe>#>m#GuQdpA3vu$Y>p7%z|*Jy4)E!aF(m(9&vG1W#hH$p1_RNMDz0fm983jui0GX? zp;>ajNUIk3Dv{p1tb4|aEh=5$pAS5{g{4GjI0hWP3K8cx@cI5ruXg@(^#sY%(hkME zj*&yb3405`K8~#2U~fHg1^sCOU#_ST=5MF7WkP*Ey>qzBDSt=E%N1C#SF5t1|2u2` zdQvrNo#D#0^?|gpFV^<7nEmI?G@Vr!#bWNCE%l!^H@5=ufA{j=-F$A>{$t|H^5Xvv z$}+vG+C;KBT<|I+liA`6g=b?L63=oZ)+%?PHG6BxFW}W?`~;&>^(XSv;T$zU_y6J~ zMN-b4`6NSMBZv(wM>5KNoYo{3P!bYD*s>C_BvpEriLq!_BUzxBx}pL>vx03k8;wK756so5Lz`tnDoxB987eg9FC-DXj=Kv^s3$${v zu1oaQz<^DfAH8J9#T1Imr>KWcj>s1?9an`6(PYR?|K*qCP?kvBeKoYDPYw zH)?f89WYMgs_QF+OE*J4k)<$lw44_|Ivs&wb$$-Dbi+3Rjk-2>lAbx zYgCJ6K*uN~!1HsNm5FsO0134zGaI{1#-61D39y_kjz>h{e3b`JF>`_@aUvF00w4)vCH}R8C8$HU^P9M}Km>hCdfV z2379s`{I-Qn1WtJj2FC*YTz}EBkEklT2Ef~Hdjv8z=gTY!D5fzrc5_ck7nE|Bv&@+ z7Xyq7tO7P&IpNy2x;&3TyFeA8^N^?~L+tjVn4e!HC({#I^oDDnDV#@8$`W`~K58N; zIue5k$pmD+$?^FXH%)Irrr`o-b*Q*!0S79MbAo6%zA?Bj)aV(_sx^u;NVSFs@DlZN zu+%A3yczSusq8nSxk?{pmKtWLofgH14_OA)$fRet8kF!HgW*h^;pEL{c`4)zogx)| zC?}I_lnYqG`4qD5ot}&!I!j9d7x8W=t((9H1r~gz)#?%0cu;bCfUSmy$5gNk23%k;npmBz!6HA7$meSTcnI4dSXLbx*DBeu| zgSCO{>V_gKLS@t_Xr6`#k~XT;gFw-gw#z(XT`HeOEXOuPf8$fi*8t<-yX4N8kY0}? zX~YPZ?5CWKfbkMS5_HrmY}u&%0PGfzP2#sKu&4=<=hOP`J?zBu^b}vywk?4;P`FPG zZC38H(jcl9G?ZRTsE(h}*aHkeoRjtI7lx`ZswbQ7B^}6V1_!aZ29D1;`a)r?L+SNO z5OPsKuWIPN#}Qrep)7_eu8hfAUAnwE%QM(=kdFoiyKXB>h}q(aRV-~jim9tvBbwsd ztsi*Bu2uK~o9vpS)ln%?U6zl+g{WeBrMYH*1at2>n%RlpBn>%KFeE)ufu+3!CSMew zwT1Wr;E;2Ye@d*X34C!SQ61`bo`Q4&Ds2ulNO@L_PCiqtdY_m9VwJdPt!)rOzc(?P z9JiPeIfI>%M#|mTyI&4ozc_mF<9q%B-g^G`FX$-+QPVKaCEe+vOnT;kM8#&9i78PI zN{}ArzY#BBNKq^;NtVCQ(*ihA!KIiIqBSH<%miyP1uH&1&?r%yUP&Q@UH2}r%$uh* z`~mKdiUl_$J$3VlGpi8prOB&su}t7h)a!0=6*+ScB^2PL=>)?e6n%^b%r3tTxj7nxFo9dB`!llL_D}TmzR)F|D?~;1N?YE zs#gKu8eAabs4OLQDwqow-m<(f)SaD(70k68lzt52X=nS#qN)fUO1a2sC(D zO3@Aj0AUTYG47;9ULG0pLcn4zfW5ANBOh8Xppd`0ymGJsZ2rZf?z=M|jrbYQL1p`^U#jJ;^ z5=Wat??CTtfU!Bjx5lp;c+il0Ko7ZingoohQ6r$h!NY!}k|Y8d@LysDdZK=mpHP{X zZD&^mvN28a56KSfnUxV`MZ^UF9#v{A2xv>g(ffTN8{)h=L(_`PwE?-Fyv2e$3QGl% zU3W!TEdk2pxr&vR!H>lV39BwPrxbwMi06?+H)I6CM3}7E!brtzIOjB;XzT1#G!IEm zs=5cf<)x~Mp8W21pqqPui^l_hH{)hER#u!WRsxy;6zjVaDx7pc*F{m`x-iJE(eBvC zCzF&5rg99t-We$CTNeXI(`7NRvzsi(o0t*dW|KF<>~q-8$d}Zo3VlOx0*M<~V|9VF z?1ePLiepr?AQVll$2XJl@JxFkHM!Y%0$u+WeZ~C|r^Ph;HLq%{(KWKtcnio&g9b$< zn!2t3oePFgnl(zYSJW3}mPz%$^gG@&$4b%_^vIH3CD{V{48a~uTx8y3OWO-=>J*d< zq<4bL);DE2R87%Ppm?-qWwkqTi$JtSL|;JZEY)0=(i7ERQg&;s763fNTSeL4tOXINJhNp zT3^(c0B4hgXqo-$E!Nm!Uh&fMGjnR%>J;^zbCAenMe{1n8SoIe3jz-kf~1F497tUv zz-}-A@>Z;@Nv+-*tpDvyJb_POXdK6!TFCvw8imMQyxiVvOIzC4*s~_CpEIUG*4zhF zc6a-av*~$#;XYrIaq69=;)Ura8*Q%+w}&v{zA*zq;~mcZzWx0OzdU{!aQRvoA{_Np zU0QkWet?;ddCxRF#$A>YVp97y_t`wN8mi$5?YqJ$mr3;2cP$|n5a?u_R_4zZ9<3yQ z(<8drWMkZHUR?y`V7n=yj@8;PXc&Z=vrerNW~kwM!z@RD*}%)AZVKF>jmUM`=yprQ zVDF3pSCh^6{+_;AN2mKw@Uxiz7g}EzP5;{B|Fwm`x%po=x9|PG?&5R1{$C1TIyrx> zk4nKyu9D*=#EsO=K3!8M`F`D?AeXA5~!HY_M5bNiPJfBZBsQs0a&8ClB+m>a!D! zoPm?rOMT89^WJX#->t1j{}+gIU&fI?efj(qvWvL@Br&2}|M$Dc*v9sqIXWXAj=BuR z#A1~)D9JFO^g*{zjx+jjVVR#PeU}X9L6RWVi zPO1Er5W5Yx|7Zfut>y%pc)-dFgb1DFxTNF2Qw2bW0p8w1HRls>xN@8awjSX{u{{Uk z**BjZ{&e``!ST_{7bnO6{p#?a2XFqF?5h!txUFaED3n0Mz|Q*2lusd5wfZajb(WV` zD@w~0@L!PsG`tto34R~zqoMNdz0uLYHtK4&3wk>|HG4EV7m#?Yy&Ea5Y9*1GmmG2< zkYLN6QX*Eh4-_dg^%y|a5celIVean#S{M9V%q_uQyqjdC(Ce1lkuHLGnbighW7|Z7 zHC<`gF1-oOd(QesdiCsMUd$^g@9H0>*`@q08Y?Ibii25E4ORth)_6eSXl$K}B^}pD z5H`HQwJE9=vygF9Dd;a~dDK%WLH?Z>`+cj$RJ#$HG_}k2M9-)39dcn{1BG;g6v;8&Nbs6aQ;a4?#duf*h(BtO9FxHR90F*)|=EM}2e!tUwm=I563-xGCP4r2EOR@y6(!{4Ft^ z$Jt_ov+qfdYGCfJvYHnC!1uf`kLjnP_%NS^uVdd@b@ziC7;k3tK_xGW8chN5uK+_do|jecJn@;6RwwSpW-YAhHO~Gb(E~ z&WQu=RhS`n#e9)sncMueS-ctRHN`ikCfEjVS9K?tF$F6#hqa#OkltH}5@{(UNlZmi zd{7o7@$EPVvx>nnpm|MNvO+W{aZNqX<|PXsV(?ktx87V*o(W?Tfvf1wDV--uWWL^w zi_29Zv|r@s7b}+C?H7iB#=Gp+sbP><9g-q)lLvA_Q|`RL?K7xHH5e?|APL)r%7B)~JW zs?*5~yo2PtHd~>UoA|TMRPY`pwDuFyfkV*IN7LsHPw679E{?Be+2~nXr$0aHX{Awm zny+P3DA&*nDVmIv>`R!j&S+)D&Yk|z{RGYk|6%JAWxqkCaNi4n(T?DeA_% zf#tbalWjymrp~C{!ivr^i$`09w3RaQDh5J?`tR<699Tt@1M&&T1Z1Lm1;FZQMHxEK z_6^NhU39F+Oz<+SnZ#r&h4vuA8Q77`bniY(#I*Ya+t2j!;yY;>O z#QeS7F@1ib9QqeyRMjmv!}kIMzFQ2Ie6CF` zF71DLi03X+wQtZfsWv2)}#?{iUnA_A~x)>K)kC@!CA_*?E7s7y`L`*P2 z?@Hs`KdB|{1#DE-68pr!dF|{~d|qky*A&>V4mdT`ml_Xd^RiQY##D&*rxDu9h|K&RVf8g@7#QMLFwzl2+?^_!?oA>p9@8oj_{(p=w-FyITzrpLd-f4%0M!!2& zl1|cEJZ_cI{V!%|MkCF|s7x=hZlai-a@5ew=(uFM_i zTo=JIFUor#!G8xIL7+#BHURG6yT=1l9T<_V9W3;H zQ#g4{fMfnH#C|wT*LRP5+V3S8&eV0Q+H>}1+tt^rqYubZAYeG2@MPvB*U>h=&PMYg zxqWM8^LLN6u9@E_icpl!-|Er3z5+T@SGrjfEf@C1r%V3ReHP>Ye<@Rl*<3IJsG0xY z-r4cUA}w1)1l$Bu{gr?q^B) zmPgsFsB&`7fR_ZA|K!0OtkL7EoU7bd4`jkOtVj$sc|e+gpS9oP1DphjG4{}@RpwY_ zl*A2K*8$~73hd7HCb_FWvFbk3CO3Ur;35izK>1d_D(4RaUSfrSRTa5 zMCt(4g;4ldtRuNOc%eRr4MNOw4tH6A^2)srDzPc3D742c3pJF}a3aWr5E<8!Bgq2t zj5^&GrsE)qmvT)?G1)1YnV$)Suktc(yk_SUPxkN(d0m|2$raF~0&7M_1ry^}pyrC7 zYb${HAD#Gqc{2F*m(J1p{NE(2)B0C@h8~?rY_+I~eH=mpH ze^Gqt$nmw7P?SF@MIw?BIUO@|O?A)ps*DPHk~A<;(P4s+7&aS;*BAV#Rc>mN-%ySn zPE(8gyFUt&;}306ZAI_$>ey+gCOp$KHHJ00Z;_-&XL>FBp^b)CV?JP=<$Y!K$Btf3 z5`TkYk~P^lkdO}RJ62UTIR%AuL5fHWm?uT8PSW&-@r)Hjl!|x&{)nRtx*Gocc>Fx9 zZz-2vu6z4Qby4=ddrb8!MyYrZigeVg=BGXWqRM~E;C zK@t>7@FMGm$-+VL7B&|WYViguyf%%IgJ1*c$$?S>KsF!mgxH!RtE?8tt6aDv%V*VE zhWQ=mleMBeUq3v0^#;CP6&rm=`+biUeSVlv&#?>_6$p7#3*lIhZ==vOmNpS@Bg01< zTPX?$tb?jC>CIF%VY98u=d0t4s%(~)RCgt<*zPNqrU&@!S2h;ETZN1XK>b7>qxy$h zFTEPF%!uZMm~}I&jL5?=%aG`T76c)v!i(R?QoV}tgCE?+RIE4HoPN?Bo=ZI%Maf|A zqi0y@`4S>2jY7_=8TK>rE^8kH>W57bzHh60R71Vo4IvP)HBh-mK@0O(Cx)^=XbKfTFZV zbk$Zljnom_`eL5dUPEFcA89LLEN!}3OI6)&@sE`@A;tD;)zg5;g2qBg9ah%NAOH2Q zuKk#*H1;Q!)Ze|i?QY-}Gp;K2QVh(V{)sL{Gh{o(n$zX7oPsIVMXXDuFDBhe&$2%OnIV*+*2W%gQg8W&L6ueh$_|=%o!-6KbG!spICr5t&{etkKUp2JM zoZ2{M09@FoGmIEJSK+e900qDGx`M?bFQGgq^z1FVKB75Mx`DqgZ;kxcF1kuQl((;s zV(aAJuZwdH3nzR!LHwwCJg!0}UgBONrf)+jHLU$sfZq}H-iJGh|LpnrhXlJPe_vto z{O{;7PQw|&ny=8+id**?9BXw!j8$(%ys>-9g9nXA@dW&#U}0EqshG^>_>6wirvZro z#OvBOLf)YT_W>6_9+7MDQj&A|L=VLqKUG{aW-5_u9;itAfn5AxEjgUb>MI`b)bN#) z<_SL7DJ8(l$0NlDL^E3`h_^*ywjp;5Ff9ryS+N*8%b&G=ve4!$AG4#OW-i&35*pKI z9w`)K38WL-8B~+mIW#(PT6}0@p*D|Uy~CFBabFHC9%~^#97wM82f^cw$2~czU8UX{ z%UMr`nlLgzg9JoFPZj$qD+!1;57aPL7!?B>fNX=L%SoPRHSZvHnnsf)zKN}rX-@;7 zD_LSPZS(uiGus6!6>m8m)44_;7`LJ3oFwDlN0C77r3>Dl_@mn9TRpZKw)@^(l#Ol8 zLv8Ci>VQUGOjlfp*aUn@N%D2?_=g3~uQvP&qHnJ0d*IQW?h&*SyYTxir@Ovh72^xh zd3BUmpqf~PDz2;Jdqce6SdF_YwE>2xs zACKrYa(zMW@I1V^y$uUsKKNs7+R56%4B82Bc%1=qXUL{-&GI=3fFIZ2aspxqsQjS< z2Nc^SIXBet4B&q$EW9oTee(TB4G>jx=31wyc`37DK7)GQs*oeZrKFeezuK`6w1{F$ zbulcEvw%dR>UEMSU#Jc(q?H0!7jUQcQvwWdlbAotC{zvK^*P1Dv@nWIoDXNc4*lD1`xXqD-C0d(er`jV_-;!+`q;J`zC0a?c3=@p}(}#n48^uQCb?hx&=&<3Tu@3xC@jN^? zb7%#XJWV8Bjj$N24Mr%zD}4G@z-?8s+-j^oY!j$k0;>(s8zI z0@0C6z3vU3+~oG!s@9;Dih@vz7F!+-^HswuM2xqQD7$ub6`^v-5!|%-u`~6hP>>bl|9tu_88vYi>?fEqsBHe(fz|sGK7h)41*i zq&W^KI0C8-g-OW)XlN5@C1HG{Xz*ind6y?e&kq7uD1)L*i8iNxLAE% z{H&KJ$+o|*Ow}Sd4O}eAqW+r*ZxCrmWQ|>^J_`}e2HI=Bu_5}~e5pb*cEerL37NTy z4&CY-+%Xu@y<*EF*NyvYN^RMEH>EH_$gdp9prEzJ@)#Pkyxx}lvx{Z5QI8TfjY)V} z;Nn&>G+yATQ;=ba(uEJ`ETNVM&LK%b)dnl6+h~zfJX>1$3GzvRva)Gj+qC3$Cj-iv zN89Z@BQVGes4}+{Vx|b!UDBgxJf{kPJ+)E=JN;3Tl0sfFGpqv86JBOPTN3PbYYH5ZK?vll-GmaOZ9H$8&C1?WPVPvxOd1K02BO?; z;}<93z_vDJ2U{SPj_!vl_U6S`=7N< z($C;#VMyN!k^m<=r5Mii6l&*Oh|{b3$wijpHbKj{_xk0_<2{T(fzXsy?UR>|is63% z&OkB0V8q8N_Ivjhqdx!xs`pV*Ilyt>qExZ~mQq50L{e8CsT@S*u_V6U6j3ibA?hFT zv;)$Fb;icohEMO`CFryz{?ktAB))kfunpX&76}P$&i}CaXv59_u(h$X@%TRe^DaJ1 zj{j`Lm+mRR0wJLoVcC{0qR#8F@(<#v$?UH4gwe0ZTHhnB|9Of04j2`#lr?=s>S7_V zHKdp@_UXnp!c^g;ollT)HT=}kosCUvFu&(N(5(Gq$qrJ6fjUpfTE+_wscbW}!Lcg? z3#GBJq@Zp&T8HXx9V{8#dyBce;htRG$-fJ!4}KP<|2np}`wP%W|2KDby!ijE&5ir` z|GW9zHvPBp@@sxfUyGtQso&3I#cR&HiM~kBbqJPYDyvKzZXw z`Ow5dtht0QErRuKt#Q^ykT=JgI)UmVa%}4mO>R}l<%OYB94yXSNnqOGtVb}Up85dl zlS+pZ6ZC!ZpTz((f*2shUksV<-FrR0=-OdVIf^fst`Qx22@6zTs+HP(Q>vr2r7tCv z`-o$?WdwGsYhXhdw#HdE5PP}7*Lofb27P&k+{KeCgm7bAsNK{J!{Kv8d|pDNHrC~VufFl^1hp54QHELf_vh;bO21s87sA6mY;8df=^ zYhXZ^qvtcf-9t8>F$-A1vpyder{I**?XVTJx}5R7;`H}aoGvqP;@?TZ+EANxQP&Ev z(BPxK-&8#h%6-wG#Q|ThL(ND8+^nTxLk<7cK584CVTCv^_JZSqcMv{0=D3#K%gc_( z>la7byn1Jv#kW9iFIYA-4jFItVQ<$)L(VJ74w+o7MS~v7SGl^#XRBB* z29Oo4wUm2jE`XwLlQu6$t<_UO;1@Aecd<2#$%qhXo3-M{^%Q1~rGQd94(H~Wf3J)t zH(`sU&vz)V8$4l?d#H=|@2DU7uizx%w)l*i-Yy*;@9P}DdZRiX{Y?(6^=yJytHYT% z?7s=N2>WCAqN0eM9D_nICIX?rES<|1zh}t{D8mK(1W$Q+k&m;)@}EVQk_xM;!^x@( zM=3!D9u5syfP&5^qW4KMpN*ih*B&hr!-X*M8_x`k(Av_ z8{KB|w31%>v7o|Hqhbz%HP#ueRtzT^F((9vzU)B(oqUpErjv2@YhGVbUOkM*q^x38 z{Yv7Pxi=CJUHrTSSDG#ePY5v5ZiUyRQf&{KZHMb{F)$sUittUv$zsJW=+AdwLb9J+ zW?1c&&~2PnT;)TG=9cgZvthO5Q`b$ivNSOajVvtlN&!l$dE zDMA3TH$-3QAzd}SDQ!^5j&X8oJBR*a`>3m3QPDON7IepO2O8s{6v=d&L2CZAymD9z zwH(0!*gJDs0e}A^XAk6K`pzE4+>7_#PzSvVd`I;7-=l7(_~;$EB;9ym>cm(1Jjo;! znUer*Orl>p)Xcz?Z;}4suyk+m_lFhXn8E5;cucga;?vPt2T501>rE)Fl3csiCBesZ ze2-v9L(|wH+{Tm9@ol~dMRgQz*L&}q$X_=~cio3SLf?!veBs2+V^6x!wFtL!i&Sk* z2(Vb+u`kOMA+8p~;ap%<-jXIGK49Th!nl_y#5A(B!5FlPt+-Yx|f4~B-ZO`gT!!eC4csI~qqtUI>Tles9 zL}_o8zDBXs-^!Y}F1lHG{;(HdXwkr2c!D6uE;HVfB+~fYeOgu z^`_!_(d1KoKWiiA2x&}p=VwYM5W*^)9Y%bLOv^_+KE%}*AB->4t4f(L2Aw_Q2N7Mc z=gRMDd_5Jgr~eHHBS>vAYI9|L0@Dta$#P zTO0TE|1Lha?f+xq%ToM5t4#3gqU*XjvR4b194`%0J(I0n;@^+>(sM1?v-ud!f0Y1F zTINNi(xyp)4fS$m3dYd6_=`M)AVN%5RE0<`>1y&dC?4}-)Q6|wBK=6SCgMFS&$H?9 zN(c=(gciYkXJwZCh7J4&OA37Gn}jkWOeMHlF^bs1d9yDOgU$3LQr|VaD#|g{H?J+| zr)oX0Dh-Da!QqVJbs+z+_C=L#gZ5`w_!;hd&IwDEIkkUqt1?9k{YT|D^6S1RAy2L6oo1|F$c=m>6#(F5(8L3yGGel5B8pFWDCi^OfQFxM} zDnKf*M!b;9XA`%Ex*qk8%psM^C+ie2B^acAg=cq?S8%GJ;FT{*C5uMMmvoe$L3v~G zn!<`Ju?+T=0ikbH4Ch+xg#C}QIu*yWrdE@&FE8zOl+DJFX2^Q&*c<3v?6{VZ-xA2E za7B>8aJYuP=|>DrW#t0#u8B_AhO^G$z~iijv|BhhxlGa6f03S9#);rDPg)dv~j2l{t>wY^rL%A&2RZSn&0ug`-@rsi|+!SfVvp+AM(jX*>8J`g+#2t1ORz{ z^y2vN^~vj(KOH9f$%kw>Og{)lp!dM|f#2HL7_ScAz6p+D{GbxOxa)H&V;zl-!6>5E z?N%6rWW2~_h#L^`1^y9ES_tgV4u3d!`_u8s!85TJT*RQC42*qAcRFL0T)&FPYsKAQ zE>pOjmBMP{nt~4@BooFgEN*cQCN$uDu8cJ%lUd7cJ6@V*F)huNc~!Ogk>99{Ozc+^ z+>$Az)%0*o+1oDhZgdgo5I8VwCN9u?_F7!Jo_|hG&5Ce0;~>5Ef#$|32b+7vWZR0F zT%%-<4GUvd7log~yOCAW2}Bwa+1)ZZQu;RW8klG{5ri0gRaIRVFTXs*gAfP%2KN7D6eD z$1LbiE8tTutP%Ear?aI(e?Gl)_{%AO{}Yf4vCcp(F82KVE{dHVE5AI()L}qZuCD@W zK)PED<5H_7Tu^_)n{NDp;81@nu*8`7BnGb)1I5gMF#Ou%3>FFR`ZqGe{BEB`?LUIq zc_a95(Pc^T-y7RIk6ru!*2eC={_k!+w{8Ei@#Q*7zZSzzMWai;yk@CXR*jfsZL!sQ zf!ZQs)f{guD*lo~%h^9&tHwYy#%6)oUy`5PTG`N}E$Vmo4^iJ#1N9~qP8C$Ze@#Is z;DqH%@=jG$QzSS-pOG@7I0Pla^|gyYrcY4I0pJR)tEsLuCG)ciA}Rrx-ZXn5sHB4c zM=Xb`OPgj&x7_Tw!#|J&xH$Gea2`Mb@l1XLh zNTzI=`gk;CiKYOB5)IOrTUFK`#fox9jS$W>G(nr4$`&ir8Pd7; zDOv)Y&CtHhs&B@J^g~8fNzY}p3<8WmEw}+8AIP}bDoZD0N;Baah{gGs`3~jI=^tVk zQ;a%bMIPklqkqeH*S#|O9anbO6WQeC_2G|4Z;oI8ce3ww>uvlBf2NO~>^|Q7_Q~jQ zck|gW`*t`Q?rx_CPaZ%0Zs)s=owM}W)+2uL14*y^tV~@0LiS;qHDO8zv|r|zx+RkFW#KIIehW#@bw8=V}3YzdU*2BgE#-Q?a*`p6p%u+(KBhs zMt^E%iP(O-->Q=>Vgc$NEUs((xtY*bj#o0>- zhnF%lWkrG`ae?xX%q(4QbIkpvmvS_j%xh9!V*DVN?wq8MupNsjh)E~n2}5fQQmT^^ z`epRY8C!~20hFQiSJ6f3_51+2$5QYfrIqx-#YXB4#2p3}xKKL>@+6m{{65sL(Y3^u zXQocm3%mXx5g_Tk5?w`?AG|#a%oRv7r^kutM+Pp`2YAYq@id#v>MJ`obRma=lV*>C z6VEOoa8$N%mvth|5{4H6%pmRdmId1G1|VT8XwB-;4kJ z&F{h)CX)P{Z@f$|ZtHhm_jmbw4B>MU_nUf;5(ze@F5?GXZ_Oq2YeFtoR)Qm4?m!ha z0@gxE)nO9#U_}@E!iJ_Wy_~7GHX3JYO;IKo1vvVc4iA3ooItJBEknOVUDidI7qvy< zughB-zlCk9Bg!%oZ_gwhS8c&eo8Kb4IinIR+~}QTUIf$mzIRdAvubaBJr2PF>jNZV2#MFGFE1p*Qen#gWxi+J}_L#Zv)`i{I>f! znT%$*_Pi}Rq)xooX3VT?47`LSew*EQwA2KHJ>o_cd%DtPu{K-Rux_qm9puMVx>v}% zEj{ddq!6RfzG#_lohd}yx>c{+*soFMP0iCaIV8{u!E6TZVpX6Rg}S}4tYrv|*2RiZ z9WjsZ9iZy{`tAU9CXw1kW@)2HOrcpi8RoVVlk16jA{-M6D);db?Dz}_xYV&|nzvI- z0x*E-!LL)zInddVq`H~>l~vVhQyf*y`ezh5VDB}vdtD%#pDyJ0}e zKAJA!oRK2xYRq6o#@*coDl(dWCZJO{PTAzVhAQ0uxPafK+Xww`02u%c9@wQHK`OsS zH$Ah8s_it0Xds@U%nnF$iYs-w;K70fw{XEN)jn^&)&y!u0%_q1fvc^u@mZ4UsUEmCr3Q${ z3`wywFqh_m6u}xQsAe2VWPm2AjY?ZGghvX*8Z2=?^Ty+)O}3q_-RedMtFFjbHjy_D z7H3=bsV;h){%M3xRps_t06UyheNF3H-jTX6i5;rjuCs*UDp^9@Y(z8Ibq@@LHzBT; zjMbmotq8H~7{=f}D@%31eOK3;u(*+*cp=Xg;eu5%bL*wEuM+LSyi<0So>T+D2urj) z9gx?Su!wWn<_;F(x6MO-65?#+AwNNfhZ`L;4B;O$tAM}!!;~w%(P<(lrnTPDGwd!c zDUGmqaw`9qFg{FWD*pQ+Bw+o_6+6wBvpY$utW58R|8O}kg_IlsS!Ck zsGVg#12rCx1=a@fsC>*~IHQJ9ZyW(rGmh1a?$%ODDoCCBy9>#m$-z6aXK)HKw^95I z6qsaOj}1zJLe*T^fb-I|aQFanDzVEIYnc-5tk)1JZFqYUpu%Kc0H2Fm08Q-GV(2bp z87crwA$1#gO^3iEVm<1CIq5)P%ydk}wzDheuvP+y&qEojpmV0N&8N;88yD#q*TQdX zKR|g+dd!KmTIjL4PoEyk^IZJdZK1Vypu=!SlqlRfoXT|CsuYn(zKC8#-3z`b3n)jf z*O!k)hUFb7jNI=)>rD801N?noPwPQZ>U^_bhgmkN^hJ2kiE4Ort+H0EMfcMkMmJCZ zad7T{MjR;bA|L}8a9fcD9XweiT^L{%3aAZk)P*4@(pp>$ayxRb`_UQ3V2WX(-Uu)j zY$W9BD|&PaN-w470Q%g^E6$^Fps*}vgAcSKMW8_i#zTh0o7{)zUu?3Ag!KmHv2>~t z`h~G`9oa|jYI@T;KdovwbSoSqgjC(b((m>`dPEF z$P5n^!!VqD!<>OB>#`wLPV?Su)%d1&T5X(e1y0M{8fyFK5}7k~8{J@LaGYbuycdBh zw2#U?9Mz=gRH*(5g`P;3)-F5}Eak+^bhxQx*6eFz`p*`Mv2%JdSmfQweiiM+Er2pI z_|isYD0e6-pB5iziGP*30V0%8Wk?n;bh+#Kt*YtmwOWlLhi zF2*%WKiRj|x9V|D zIyl< zyeTeIa>j6Uz1DE|l;KOD48OCUCLoD_(WI!eK4_vXHTb6cS{N zA{hoQm6NYk<~ES2ClpnIZb&pfVVF`s`J{qv#J??P&saSm9EisQLJv)bU^K%d34Yom z%ttXeL#G}>D#%a_M{m~ap*m#Zp+(ONnxhW~+CCswiFySJEu0x+q1f1~n3%Oi|Y9oQXb zhib|HWY%-p-KB)UKjgn=BT~+EOAOUk|MQVo|7UY|`#%5EoqU#@|EUAMtaMHRs`5hl zHE$qIQbWxfF|j0W`BV^S!B*%rNIf8j@(oyr#MPLn6X=(qi9<}u za}M5pQ&0rR8BY#Wy#cB${c(Hk(b`EUUycV7&N?bCNVxhMm~5+LAU8st1};lXIU-eo zIOTi-0mx<87eji%8#yp;x7xusrVnKdBO_}k9I%QXa9ID!# z_(p)Y+o()0k&tq#H;@shAmGm{%-U)fAj7&doG#BA09? z#xlvdAJSuS9y=LbV$~2MS%{K%<**MJFRwxL5kFj}SA7}Ge?CvK%ox@gqxx&fA(s4N zZe2WiBao-SWKg2}UtMtey#YaXkd$*OMMmXuRNiDPoB}0Tz#@a76oAAhP#0LE%yVwt zQt=-#PF|xqqAs&+4KvW%8^nSfSh#{7k18dQ2rvihPJ8Gyz7SwzpV+8OC% z*fXD+gX(D{I(XSsns<%!(?naRkN!H}M^mi*oQwL(Ggc4KBvT>M*1YB`*9h|Uk+ZsIC}Bp9;CW+hY+%}Op`MzFiE}`CW9*l zm5)Ep$53DkGehJ$JT7^jeh_C4nUk)HIp{{K+SX}fEoIEsnImaCE5Jyn>N?UWNTaIN z?CeZj{AyKY2?ZK8OKT9pVHla)^tD>nkAi~w@xLBYF;-k&6qUpyMPe0BS{ynlZz9OX zU3w1r@NQZ=wlS^T58Ssn+Cg$S{WE^K28y$v0bP4?OwYAcQzVH-y5XOnjsC-aeEwfc zx9dORKa27<`Twn5%=PW^{{ngM`Tslk+?@Z9;7dpTpQMLjc{Qtx^D>=XT&*8IefH+S z7Xtz<1-Ut^R}YU~?QU+xI@syCso++u?If))*59OO+0(QNr2KBl8QzjGoPIveC%Tri zkI@9zkBY4Hf`y*UbXfcQdXZ=tX6)T_ zhO$DS+p7SV4YozhF@i|WULm1S1!L5EMFl@N zA!~9}PX#Pv z4U^hhwt2p^G*ri}0ee(|hi=2@GArQD@2!7A!YnM zM;fg52gO|`^KqREE>A6Ax!s5dUC0iVZZXa3%cA^H$pb|u5ew_ERz4}p5oKzTY>J*) zFSfoE!dB9gHRRz(o{(3tVnB-Z^|TLAxTn+e%tJME?(}1kj~qZx&%|AiCdV;vrRVmd zO}6r$d6rt;f-aro*QbA0_s?k|ZkTiRq+K_ZJbjTgSMryhW^Z+k!ya#kS8XiMUCv6U zXH!u*jMR!gmhfYMz3xhp`JmpwIOd)$23$g1VZpw^$-dK*nwmiH5#(Rin9q(>jIzMOmWLjIYR(tbIZu zlq_Mo1Az@6{S2SM+4Ab}*BLne_B>D0KptTD*wS>*#InEsHL)dY^H*;m`253Jy6PEh zz2~%kmYv}P)soD#L@)TBAx_MvAEw3S)TCsc{YIS&$wR!ErnBmzsAG52ZFhrf+F<+& zUc?wKFG6>CIPTrHO*P9MZ_~R$jhlRBun$UA@}v~c+`~$X0ei{G17INO3PO9BsnS04 z>WL(47yMe1K5Gavd8!`V2PZ9HJH8O_B6t-97{ z*72Gbl8p079<7&Z(}K`h8;+=r5UDUKU@0+U^4=<+Deiglp%1(pjR zWixtC63OSuc}Q!E1<+z}Q3_7Yi3N0VE|(V04y3eozIPq%Sn^q}BXVOMa);csP#HgT z^mZ9aTNz~VaW+f2AVnj+iuGWzVz~#le9l#DvH{6zvWdJwfNu1yq12=I)GU#JByLa3 z0YnQUCtcjUh(8`elvJC4V(;(;x0yQ;C4cNccsT|t1C&8xRU7Ooxb zsv4VL1F9|VWx;&>D5)g-lGuFdT3x)w2IAfNMpC3_**LxGtr$Oj?~XQ-&ooq{lcqK!N;XU@R^EdiRjI{ z_YNvKLk0*Hd>8`ux4cjHTbq=@Cy>@tZ+8*SQHpypQZ9e~B6S0<`&Ac$elxP@DhC2o z0Yx7a^#xC`FJ*p%xAEzKjTf@xeoXR0opz+rf6cRbHj2D31|0#_oaWZ7_k;f#;o(_1 zpvqK)uOq7k?2YTe8LNyNnp_j^iy{9Jvpa@~j6*=ylBcAVWkj0V(YB-f>@0(iz?qw!X2J+4SG9#gOwyHU#M(>X9j}u4%$Bxlu$%YA->CeD z^APtB%?PI%{8eVFTAQCdPs|Woe(ZSFt)3tbtRjF;{_I`${p; zu?(IE0dV+zouyUW)atb(Yx>dfQa#1SsV3)hru9?uwrug{^gt&syKQS$wBf9I@-8{b%V& zUm@DF)VrhJb5ugYki;^nSN$o*Pbrf+HUmRHDGy~(SkB2cAW3;f*pOUhh;QOjh$odN zwX~TW(V%fn`9ews=6Fo51cp>&b)O*UCthg}cp0YCgAG zin=8}q&fa$dvkZg&HwRed;32A<4!(HjsI8#U%JPKb6mw7u1_HDqHFjD21bDEW2M6_ zj`{X_t(}03K&VD|Uw-h0(JC3IS739-H=Pwqm9lB$Aa<4Pgj|k`v*Z)E`s{>o_~Uc_ zE3$nWR!1we!+gLQ4xiO{W96hy5g&pEQK&i7L?enDT@FV@MjVQqT_z?u&5@B2(k=Xk zjdI-vQh0Gb>tsGVFH?xvrjSe(s`BFH@!?+b3*@9ChK8Nr#p?PP7>Y#~Wk7mHG1>4U zykX2LgiJlje>1(~F7jF>IMifav{9Dy8$^5fr~y2k2W=zr!|&18t`25Y2;T$LmZ?i2 z|FLLzNH(2M@bL!b+jw^l&Qr)RpjoRQ%$!#TV&AEBCe?u!W(3HyOf(o*0cn73ncus= zynD~Oo$p;HNBl&}lJ6Z>Py9g19Ok9@BWrw=X2ly3pn}|z|4T|+kNrwpcYY0o)MM_x zS-nE;`?K0zp094QxSodS6baivfQtNuP50|SSuex@t1|pxbt*{UDjM>DKS3Av)d{2` z5`1}P^DR$bzI}0g^y0^p=P#cf_PY9Ew)%=oR&f6mF#NCNmO_6xPUP!?S6<|!z$32j zSvI^HiVM5g2wg<=Ka;Ot7cdv`nL}wSFb0_w?m(3fy4O zc2u7(qJZ&9@9%NWilTA)EJfIUZ{L^2j@?*y=Od0D?g^=Wp2~50!NFISdGh}YrqkHD z0%A2OaG z&fV-hI=Uq^oJrI@VEJd`S$Y3pr(N>NW&BtTC%Y9^NWD}pGF-|<@$e=gW?FXGB2%8p9ow;YaRd% zl2VEPEs78GSp;WnW+W{BnJg&;frCasJz+LBsY^gzVK()VAgDlq zkL_W!s3YwxMvbt8+E~0c)=t{>`ThLM!VoVIW4-AU>6KpNUAIP<+znOdS-wEpo z6e)KcTjE5=?3I~CEX*Ik@=&8vF4A#2YWS5^Sm9qyHCCjBp~|vbSou@}Kd~d2<`~ruJU+9rrJ(dS+ubYKaJ7oL8T%2shFfr~lN~91U9nKm=mRp<#)yS(ao5zM zo#B8_E7L(2p`G!7PmKA9-q2+szvTXUt~le0FoD-GB&OULDWuxH0V!mQpnIO>veY&H zRSwvYz@-6_Yiu5PhjX^1!EIMwfj*rae4;8W)ueLT4 z7!XKA17H}%AjQ8oVv6*4=?ck6CZlc`BIkO)#w%+_7PVO?x87QEG_|d$n08#fQaDjk z7m&jjEu_?a+2O_Q09rDglK9%hQh-7z{6u#Hw4Jih2Q;RdW;kQ2!Bg=d3qBqkyXR}4d#1H366bme3X_>u#!Z&N?bPZjr=2^pR6K-#6&Kwv763kGHqH`fuACTRZpupLg+Ds{dyv zeCg=;*_06bkms@eBGH6U;Y9spTNT{g*Jidh}bY>953c@gJ z!796K31ufT>WvbAa$8P%Fl4AgNuvW?gX3IR*Er%fgF;M$+Z!|v_`I2lt`>pMa%pp< zzCVt$oH&5K1ofhMSHC!M7Z1-8c6k*B|Jks`0}Q&RVlt}0x(>F20b%@;_f=y|J>f$-nf_l@8WaY@_!ItZdnDOd<#oinMc-b zp@2kK1O&#(gs_u*nok5lk(Dsp)dR`NGsyWm#QK@)w@0n20}m1b?ZF>2lBkQN_-0W& z_Z0oMK8w=-HyMV_R*RH`Y|;Pj?rgjCfAjI~<~{wti_dM-e-mGBTk|WYO6D^lB@_fs zvFdt_P4vB;hVyO$q+1JFQbTEE*Zsb!1trjpG?}@NhE+rSXFY{jnG&)^GvgOs3VfPf z8sA&x#D6xH5*&(t3XDZRSc7>loin2MyEUx$Yc%itG_P|Z8~m2Jl`*kEbB2NOmXvIk zjPoSgoQ5+Y*W5;eB-Y5=hkQiMz)Fl$QaPCY6__b{RAZTOIj|O|xwnE|Y-tPSD>D!~ zT4T)2EPeAj65dr>WTCRGXX8paiS@FUYToHidLr+hxB0-gRiL<5oeqP_G_3qBkk`zt zX)uD5ku|Y&nJXxdfNo__;4CWziJo9pTA5PGy0CHq(M)4DrEAVI=G>|X^<${nB`X_l zXOMb!2GI;L;tpV>Xn02;Nz{+g^##BrM+(0T?SN8Jn+m7ARcM7CsaBBr6t(f;>I6K0 z%*Z3y07jK%5O^S2;D*Y>ICpdz_shK)u?yCaIhvEkncRy><@;e?1wm#sqmay>JH8Gb zfFy6}Qf`=cF``$j0Z@Ydww9)hGHIQRGsps2Ofw2pysw2S(;CI8>Xqum`(|GWKY>t6o9i_dM#{~>(oqzAZ3j=q+h zfUHrtsOwp^x4wR!*BA5CwP7*A42H1G)w5iuG{i5&eXEOMFEX$%!`DYIpP6%@6yXUCOPTWd z(Tk(!Z=at$JN)V3zwc_!ga7XubE@v?Xp&Fk-8f~d+f8O~KDb`*k(8SkTq|Y<8XcGE zw32CTxq5YUD6@c`$>&;(o$l>HXE@AewY$r}c&|)&s64l^4!99xAAXhxvLE=W7c?it z>=`Qb&@qW9dq-*pYV(j;ol3=xf}x#EM1a@b0L9LJxjS&zE5mlb+y$#7PB!S-ck+9| z-E>kDW8ziFNJ@@2x_*zK?nB&VAy`Ew$cZBAvfTG*1f1lEB$R9_7*Q%rPiavzys)ky&Mxe=gK;1$ZB&Ec(HJNW^MqIoK}>Pl_y0K#nn;O#8~2VdSzA}FD$Wn zEjf_lX+N1y*;0z}{$J}E-RFDtI5nV%SMu zA-iITRW-U2@_~fr5|9&s#eov6{K;+gG0COFq=JvYvxkhjH}gGr&mxjiHkdV*IUvLf zdC!`Y#ln9CYdo2KA9Nmfm=*ZA9jP>tCEEGCjL4YT zkW1LQFGF>{Sen)LBhYk$@D=!Dr+O<{KGYFiq|@_EIlT)pOrH;bDTFn(lc)oG?cN?^ zO4FL*1D5+7_*vyo&_I@P)1KS3gO2_1_0!GAJFC)NtU6+DHd^xDZAb+Ck>-lq3QqUr zoWW$J5l3@laPVk@;ESGjV^-G8W>8fRT^0Nln@ep3(aVoqH%|P6Um=S^tYYGg&l*1D zi~=dP4UbS#a}21O-vDXWKo(&d$ov{HaARJNkkf55h#ND)T6sJ|4!Le}5rShwi7NCs zjFgg6zQf^q_i~zGN-~3w=#p&)R)J0TlZ~#EX^%KrXm~rEl(sIn{{>Wa{W7Y# zE>Oxx5v*{{W~bI_sFn}WE0>^{ltgLYUPUE$4P? zgb%2Ru8s1Y@HEOk%aqCn0}YQpRm?Tf(?nmvtYO_-#&4bz3+fT9|HxHoum4UGpU#`S z=0kZ+p8M}U%Y6e>wb9kr!d??wHO8t*x;(oAMY6gVCiII@?(5XN<6uKDiS!qs*$tKI zZjpySAiW{!GD|FJsG?27>uxj)NyX6W$F!_g$Ktl<)AQ9iB#p0zMd_G`ES07fUkrVT zT7lKDi%=p6^{mKAM>yBMPPmlu73x4)VFN*cR7~r&QpUPWyr5uJHa_c%t;^w5sY2FZ z$`A`Ey9kQRIi&Ce&4EhI$tARGg=JBGI2#w2uJ^cPg|K|7&g zHHJxj!*Sf6xeUXxWyw{xV`X9$uUQYaG0MbDSH@OR>tBqsO4%Sx;1tL#;26Ne+*@>) zVbR5kk=Gp=H2^svLs6E=Qw(JELg;lJ{2AKbzI`-cA~|lZv+;q~QdJ(uYc4 zSQIaXJo7(5LNC&fnKr43m4u!oY~(A_60OC;@3Ah3Npa$Je^mRXIzR2!SuMKf)k~I{ zo(Zv&C+KR9HH%glbKKiw|CshzTV1KK&_TbO&;dY%o zd->w<E_1hcbk7xDl zi&dGZDm#85_pgiH(=!4hI()*V>}iYR=j&nIy&>?=V+gdkE|*+=6>zb04R&3gUHAyP zkU0Wun4P>gyZu?MRLvHQfO!AAnK0Vca&N=9|NJhWMeRS7^Lc^fZ~sqg{?8q-`8fIC zHpSa?Z~wW2&u!a(6uw+9`I{ZmjR`#04_`ms+A!zBFVsX$f#A=@|16qTPMTIIl`%XT zrF0s_tla4|`K0InOgTnxW)-09$$o}8Jq?S@`D8MeiSA%p$%UCJZle!23Fx&cE9$=W zdyHv#E%;^A-Xwv7kU$Oq#&}pIr}KPVgRev-)7%U8b|~JkyqYAv%Ffbto>A`W&#{!| z+(20dI9IviVo_*(f#oM}?xRm9C?&W{^Xvt18FaFc^H3Y z+TP`JE&4A|Zg*?5^9#^I{~tg0{J%DLAKml+ck;O{`cEhl!J4JM17b{b;!&BN)vNUV z?<*7V0y+o&b^e;;4E`2c%Lq+8b8pa~+C zrTRpd8BhSQOh^abGaiCHaEcnzlFnx{?FV}U!N3NSN#=>NMhXqkas{CP(|=J~6$1Xt z{*BTt>l~)Vc;vLOQt>;#tpubGZtW}rqf-Oklg@~}V7IVI#c3MxYwD7O!2?))x@Xz6 znB>!&O}M$S(KJWzR$i8#^|TxVroKT$?OD@&?Zb5?Sr6?#yoKMR8z*CPLBgRf8!^AXhUocn>T zxo$iW;u0b%4QKY}m$AR(kI(;+-{$%e6<1y)&MWq5=Kpqfy!wBek2kk=@A<#G_}rZT zi{eWYkLNORNON^gWWirq!zH6^I8IA)HDOnOifj-+)fw@XZenG7h)+Qw(+X}G2r`Ai z-LH#rHUI_;B7y|>HyA)X)L`(i;t=%$3iy%-FL0Ry4hh!Xy z{OHBW>z6+r;tUw8D^L)bRT>;~caFytgN4pkv%FT}!=GSuoQAwYMxQB6DuV$sxgAWr zI_8KhH}cu%QMcDNfk(xk&Db1wE>3wX&T5qXDkdU8xZirv@SB*Rj)n5rha&}JgxYzT zPT?0i7k7UHNRX113cj)@IeZpnehx3FRD38_9NgRbAQ$Kp)}Da^_rTc;lN8`;$Uo_m zg<`8vFkn7|9eoZ3I>8S=(!wKfWTgXtt8@YG546F>;U>7jC>{T*> zFy=a1DA5{Lt(I#qQx4iGt9NXT&N<)-%W%Gm*G5rmxM0L-5RVlV(r5eGMWbT~_yBSt zWN8?Qsb(Ld1VeX5QJGjE+_}-l0!kbPc4}@H40sKBqwuJxlX5q6WS^ppNyVfL(qr5{ z(LT~(tTRc}Pw-cYodh@YxG=eJe5Z7KQ;PEZK=D7Qnz`{I&dv;4QUz3J)yH#WEK>;K%z=a%F@8(%tR{O}~mkh;G` zQq&VKL1r`tgVYRN!^tDtIi6xe?#gmp}(Mi0Vr& z=y?XJ+ov$*=e}S^rmI4R8w;lSV-|W;B&SSo3kD?$K*$tX(<`WzTj#^MfK+k`*-2!E zDe(prrQmh3m`AE&q?oU(sV@YLxO4t;@cPBkiy!xrUko|PTKeaP?M%`SLMWY=O7^-c z=3KM_h2sC6i$!GsU=p3viL8G|>?(YBR*c8RWzN;`bfKp+&_J#g0-t}A_>4uL>UAMr zL)hf>d=+%9tJ#=dj8y^KOyPiFR0X70&F-t3Y;J9R_xK%7Aiz@9?|E)`WG4`1pF`T0 zNh+}}$fubAaUK_y5D^fjYek_Z=6{L?0F1?s54jzp=Zq z-vw<7&g4Ge z)FtvdKZkdgLnWu$(FwI5n%1Gg9|KI!DYv8`WE2kTK6dsBF-a8Xx=3r1%#;oZ#^h@j zp@M=LqNfL+i7u7nZc6q1`>$XrcsgL0Hg|SzcKzG#|JJr!|8sL^^YOj?=T1I1zyA?@ z>FDFB+&H11HGp9Le{bOY8+`oxA3u@n&e44TcXziR`}cqA-v9efJ~zMraeV1W0+c&; z4uW+xBguSSX08kA0i;%MWa|OM`zllO@9}pH*up z*JL0emgnn-N3Y(%*Q+q#JMq{1xQCy#p4yBEuLU#*Qti;i1dWpafcz0D)fajuLKePc zkV6?T%Gfro&ax6zFizNYNZs1C>qFoo0Ivt&IR|Ae6rUjtpQaZ;3KxK_tocJV_`JdHiCwg0@8}E zC3ZtY=_uq*Q(?W9DXgcv%S|Z zUmoveXHWUo=|~;_hw3~PikeJDZLvoDoFzcB^emzPelr;WJ()Y zi1_K0R-EriDG2)pby7cyIad_*V%VuK0EX%h7nUmPRg0DW8iY)m^&VD1dS(!^z5~=l zGBJuvZIf-~QOb6cw7jI{9Mhs2>jF|Jhp}KdP#0~Wfaq=> z-9x<9#(X&P>OfB9?8cy-ruaN(!C5{En?N)e|<0p7{o^lcYU$bk&)%DMw{abdqW5!tMMq~P0%H&)K@ybZ$oJ5a?C?Encp{aYvvhrl z2GAfo0?u76MR}NdC(RyOg8PE4Eo1^q9@vv?GpD%Im|J=8OY}#Q>kLnJ> z+kuOUwI0opEp&Z?j&c7Q#K88E?+^L5VU(;jii;Oz*?H5rRzc6g!_$g^omQn`YP^uJvgtyEV%M zD%@TWQc*(C%VJAyUn$oP!v2Jf7ou|2L8Fw&q~~*Yuisp~dVdHqX>?xBD3hCPf??H&J|u2gN%P8%RNHf zeiut=1{6-9zh&*_hF2Hk{sh!uEP~JqFB70%+e6SP|NmKqXmuzy$oeO{9^qoCkmbJ z(YI&aCl5^&&>pf)(vSt`qaaMvaE5k@Pz?uU>mC}(q7VjCOf7;MhF76of{Y=wJVTrT z`TkJD{-F6;QNL^@Sg&swd|}5AMwZq=wAJX7+vB=KY8gXLVfn9RiPr{oB8%n!=2`Vj z&&Agw{qJ!;{^zK*%m2BRYxD9y<4e`>pPVk2+tB`Wl*>qM0d#kZduZtnhSB6UL1F_f zY3%j9e5+`6RbL-epZH^r8uz43vVgG`ixHPp>Xj+r-7UpUq77V6iSsh_fOg^ZbOmeu zMAHOAilNje(GMuUCy+x5LP?vUgB)LJdcD-YfTr+45@uP_rLz>N`nA<@&X?z}Fjn1+ zj@7)ys-HyExT2x%b$+gv6SBZd2GE__cRSZ&`hVOyeo*><)@&Y8&T#wc@my=!_%&%I*?nS zI><}>kMb|JuNwEmQb^)K-&N+Zo6B96H2|1H^IbBv@2R6yuS2Ex)zge_&P)#Bz z=we-K{Tl4Y1K8ic_EqWpzhpLdMKjQX^S{~j^M5Pyezc4K+RF7H=f8z7%N_sx;bu<% zv(Dj3*z6rzr@!1PJ^g(tuSDm+@;N8b`d0{9o^gzZ=aJz~y{*h1k+l zeA~i!rPH@MvMzn<=9!$x*82@*2l=uHT-1+})<=XuD-&!X3 z=i-2$uMMEz_?6%P3zlSsJ{I`@o;Gvwzh{TL`mfu$*5CgN@MSGOV8!T*8}J$ld|Ph* z-U~?|1&O90>pvF?(nW&Q05tDus9Rlgjw7-Rb#MSwj|Nj(B4eyrnJ+Z-=THbBO7Hii z;~x`b%iSdZV0MWD{^UMa&R}kwqMz3fBq0FAW$@U9oqWgMXzlN>2#A+@p|KA z)GH{aGausrahu%j8{q9L`0i_&tq1F&=8edrE9F21x+ql4E|$v)JIQns+=7g98$lVn z9#t^vrcqG@lq=w%1q)+ZpyDKRybAQkhXHKPo3JcLD5F58u~D3pEdVV_OZYFvz%J(h zo-_}w9rGahzjFNF+40%oj{a}s+C2Z4$Cq_^!F-E5zHn3568zuFgkS;xcbv2THjl6- z295%f!tYmIgeCR^|s)OaGy%*1pS*?L&T5^@7!tepPdk!TXFj zJ`;C_S=!O72Qz2g#F_etDoMpQziCGLMS%1U-%e-6%%2Oz1&m-wibUW{P+?hXa}(F+ z&!4}1dv*Ql{HKfO?Y$hcD6LsMqu~i5#;i@*?#q(4V=8EWLi<@X(MxGfqAWwS;;i*s z`a8%`Oe#mKc)K;QCCZ}flHmVR9NNQPmH0nd&SVeEnbZuywMl|Gky#VfnwRgkY{B@Me&7IJGPAfYRoA z3?T7xP&=GbrVPWGX4`&eGeW?$0qbMP9CfCcr_>(FU8HzF^)KiHXQD?cR5?OdU@5-E z;^c~p)Y*khp%t*kgQ77UWQmpU9yDXn9xAhRIFi7=nu{9D_9+XFt`l8*`R06&)N8s%Dgp!oS?qKSGW{>C#=N(osHdl3Axcyz zP(eNoj5nU~wlnDYrl_k@(3;N2Q0-_r8EQ>QztfD6Q;%zWd(<$Sx z1*6K+I7Gk=kD|L*s_7f>q72ysf&TV56Afc#f4;T=^W^;L1Wc)Ev!)RECriO|pEe*QG%SMN;5xKEWl3vOT9O8|H;I zSX?!QTW8Wd^|GvkA@E{;iVS6mjH%VohzN*hJJ?k`Xvlx@EfHAUX%Noa@b z-gMjyu3eQx?n7%*o&!_`N@H(Lpy6}%GLZ2d{K=->+ni2^GNc*hR9%ykvPx9a#6`Z_ zIIDtX3adC!>y|t9jR_Uu$fkyWySN*-!fW0I|A-FzBWXJOyP_g`;)~f)1+m z)emdae96K4-c-Nv$Gt2jsa5X+)R#h<{6~DwUP*OFO&bff`4>>Iwy)T#h#Xe#;;anU z%=a`3;}x+K&Q+}5xEVae)dAO$DGEO$br+XVPKm-@`kP&-*U1VrV$OO|!Qc`uxV+@E zvD|Py;bIGper%d`J9eeZ>LsQv8E#EhDtm;NluT<}9>QZ}p){^vL8a#Teb+01#viYX$^-H56ms=Q3haJ+%NeC!7 zf$6MCqSev{ASz8@GE_M!sM;7%*~ZAs>CJ8M#ZEGD&k?+)tPm=@I@%mm=nhZd?;^8| z0@Wz^!tt*6AT7CFkOCaTVoTtQ695dXxj~?Q#-~-$kqm8ujcjDoog$DlY?Dusr_`f0 zczC3|c=h(pyQ}lpS5Jc)K~1(bQDesgRQaDhw8HtyirY$lPrrdt9>$< zGI#t7 zm7Y^Fjn*JBX0E&uWIW(QgF9j05TNcZoJjw1<)3h(;J{2bIkiVVeR7ov=csOI(Ooe` z@M#rixXVo>S*WuI>Z7KiXUFC|hlCYPQ?_evn%;RMw@Z>r?o>UCuL}AfDN?<%7`V{? zH&_3sb+W7fyPfL+^glMfte^+-D#*L-?Nmb>zUJ$H=B`EMJ{H7(HBWv2|K{OIbGQGu za&1=sGZ$Yf(gH^hJSQxS*icTUG~|9XKWV8kT#eH56~4?Yr0Ps1T@yG@I3;6C7+-j= zT(QJn6qG@!ih3fO4Lix8O2_o(2D|PlW_%b9vS2(w58n!7kWc1o)}HKGxre$I^MBty zdEokwC;9x3r_IA%{MS~l&GUbZFV*XRl@xc+Q~TFHUIp)xSvXK_T{1Z@qStB!i)*|; zrv!l7mnRDcP;)UR1y(OoEe>NP+=GMgaN4~MQtlbtia?Ev3b{my%*oJq3qgu^(PJN9 z7&g=>m^ki@y15Z${LAMYgb}^P0qy%EWXft}hq6 z92@!u93T1*-}zF5z_jU13{_)?wz!xuje z!tC~kBuy1*l?*Q>Z~d%u3-6Qx>tB`je{OY_>tg}`*J|eTKef(w@xNQS9%TRL@MRru z5NI%PTJ!=RDn|W6J6Px&eymXFJ2$aX9RP9|K^XMm?O>3EqrBfR#8N^9iYtuvkiU4= zMW0Yk>??lCQDWU$&Xbu~uEGq}1IzlR%Tq*=WXG1o)J2)C(@61j{ zqn?+U+Ji=1!z!vieFFgOxn(M0n z7wUhGbN(NvM~6H4Z!6cv&ws|3WgS1%XZGhi)y|f#IsPBslC0jx-2H!agt}fY{^#s? z7yq-BYlHqD9==qh0F{csIQ&0n)#=%x-+hhAIunGOuS_#Zx91IRuHV zc%p3+a8)@D48L3?LdbVj21w=enCmY=4HG&rFUytr81qJ9v|Y)(uv0ZF&-}Y6p$^Z! zET|~#M}wK(sab9t7wp?QC2j!kCyIC>xmmJ7=%51=RsjV|Fc!$DB!IvujF=?d1pVR1 zDIuF3|3S%%l$Z&%5S8rI{g{uVSO8ucxRU8P^;cOcCt8Z?Z#pnCONXLiC$fSTaRAo0 z7e-X1gj3KRy;awV$2ySN=u@QrjYf809Hw&PT$0eS>L7|Sg2IhlQ#;w+wD;}^aH?s9 z3l3Q+y?UBvAYL9bwP;d;d_NgEH+uI{0VvL-eB^YTU>;6|W2PGRdT}t&*45lt7`Lji zkwf!BD6gU?XqKsGmf0cxB>#5)?)Am%pPmNqu_kBIcY@y3FGaEyNmKj)9tRnuMf4_m zkK`UVf<&qPYPZ5hHh>%$o5A}P(QSU%)dQj0PccNqjlshz%Qy%TR|Im6@_=%Hhi%N! zRNnDO3MHwr3%e6?JKEt)MGTt{xHltpOz51t64S-XQoUB{90-F+JdEm!t>xQ=3FvSP z8B+Z?nPz%?1re(Akow9?5(ybz&5RUEP_{=sK4D4v)oltEoO$+Oe4-5_QMex z5HV3`umbeUQv7-|ES_4N38MKkb43h|Rm_3YZqEa6V{D04K`@eZ~bh@+uZsppr{bv!rED-~2EWz6PGl?ey)UED=a`kmG+W&Dn zy1{%#SIEL$>SEzKGB~3Ck>Amy)3zkw-YYnUsqTtRNM?3oOoEXu4i3hoa(O41O!54d zlxECVMcmsa6Vrvv17Ezbm~1w@pvr?BwAyIy5VZnEm(oBkVk`-j;D48s?m93#_0W5@y%Q~_X$;egq0j2qSb70iLh zTEhYWEry7IVyU^?zC~5|zjYJ9EaLxi_1}(Kr>93d`oE3qA^5*_62KH6BD)kY4|Ofg z|E)j_7Vv+^dH(OPb-45Y*~<0c{NIWhU^c}4>AWzTXZ`%7FjW~p7-=^d3?h^MWjmQ+ za$Qpbm~CZ;c|VFF(}B9j)fx?|1-19@F81cxe2RD<^-X1SjX&C*;*qZ^{GVv4ABz7w z&FB9+J=^hrTe%*B|FiLB4L@+ZnH|&jKvy;Y@AW9aBK_ZCF8@>O~HjQ@94 z3Xo&%fQceLMTeeg`cje^3S1map)PRLybDLDoYEyEZ96B`y9Nt5cSi}7H2#zdTWRG-{0@)(!O%6jxr{#w4?&pC8Rhp3v<4h)ttzfR3r-UePie`(=@4bmJ6 z397tHS(_{Z_Ap5~sIDKULyLR#4bNczh6NzMEM0d`2S>6;i&Y$yO!KC~m)4b8#Tu|b z@dK0Epy(r8P@EV1QuLtNT`TE8Fd)?o(KADhTU&$+-{RhoHp;4R_#fnr<=Q=Eff;=b zlew2O;ACx9b}|2VcJ?*m|BiO{->qDm=l=*_mgf7;&g{>3{N9$XdHkQdB&+u^KmPCJ zFt7hVRrh!Me;d~(`9Bw5*2(##njn?l;yPfzlWIqri<)Ls9lB!D52$(lfs4s=arAVi zJN^aupg}Z&#)t*FO$I%1^`i>+5(%V!a-mpuUQ9l5L02fzgXLTln~8N)tm>x3j53p? z7}2B~V?km=Q(?AQ829wZ8su?$!6Sahx9?#*(ApP%V3lUQFXU;}j{H|jVhvFvRHQkC zY^5nwvsVmZW=fMwYjZTJ8&_&(LtwBKD?JXWRK3M2gDAr6ZZG~6Q$!2msx&u@!ck@{ zy{4*;8cJ6o6#T4fR6>mFMU((TJ=r5B9^$B^L2@YILSz_}rrsG$VucDiCWm4VeJ=V( zg@nT-n`kXDZ*!I@h_gplJo<-PhTy2omtL{$(?OW3?fx*;@_tnn`ylc{7yltohP{$O ztr)bqozi_uip#Jfoz8Tk$XxP~svQyKiBc;e4*OXHLRziXr8#naP%>^dh5}*`;;7T| zi#XS{@-MeR(rIIJ(QrJ`#e}q&`vRU@lur?_OH8qZWKZ9FCqpTZin@fSu2Xc8&wI}0 z-I}%Db>I;)h(h(3w+^;byH(Yzk{3MG1-HI|_=LHx>vND+nyZ4kRdBWxIe$Q< z$=1-*r%svjri|gE5R5ErWo^tH^X zEOIxSek$#uvR?DLEJoE496%+$D8I$8e*&lG|a1l6@9@`Lap54a5@3c zTt&pD6D(x;DeeL9DiRzNTO{~EYij(T;!v9U;sWj-vdwWwRyWM@NP%Yq>(l$;AS*j? zeY`1WP7VxA zy4gVg*I`BNQ+I;}VCS-`r4Ma<+5|tVOS`*FY$5lWi}^3umVTx9${epXkhK}!ufyu< z8C2Y1`t^-(F@F(C5E`bMM`#ghwv4uvg9wOQQ6_XRu~q3C#bLo#&a#TLDw<4w4$jbD z0Uhb(b9Cj{?4k^6u2h4)7xe0ONSL}rhm^=|oi6jd_fqpU;)To7o z8Z@V!3v6i@luUZeHT{aHm$+xGbcS{;+y5SW5RiC=jsX*_F|nHG!p$J*pno9w1L*1{ z<=z6zxC$8X&#k6PCQ%Q*26#HbBf}k(MF@LoYe+fl(g3aB#*K1DM-Z4@teE@PkSR$)n@P7#x?CEe~+E$emIS0lN0LaJzfk26#% zqm%1&+=B@~mO!miWU%a;nD}}SO}dS`pvp8OuVAPXOHQ%lB%AE(X;JfqBed$a%-B`d zNjiHQ8R*!#XBB_nolZ)tz)WQGt?BYQFLTraffp)zyjWT2-3cr6uPXfi<;s=6=vu`8 zpR{u4f3tPGJO8(FJp})Mxm@M1V&?yHSLSsaS2g~BJqoah|35v>?f;`4|G%B1bjmmR)=NX0|Y}h9vMiGIg>C%D<{4a5=BE z;gVv2!`KtB9&2}j8gc7;wy+R(lIett^l4e4N0v9m!!9QbK-$(dMS)HPwHvl23#8H4 z7X_}8V*Bh)5ct=;^5_4J+E)r>3MmgRe!}0$8oWPT^W%S7C;9xZt;5!?{_j?<_0Rt@ ze5uI$7Zv|1ule;vcziBkJ|9<&-3m%=UarMF`fJX)s*tj)QWjeIOa!j5#2N%Ylfjx zZ88t6yxMc&%J0 z3@bb-9(vmS2<6v+M@u;)fMQ7|A-hE?BGb#5>xCXM4lC(R2gC{$&G4_P@8@bvIZp-{ zu$2(hLRq}`L=r=oSMH+t=2rd9$e@rvOGZWD0r8<5@yMP6$#Ed}+KQLE(L`kgk_$)% z$kfH)Y99}J!g&HGc+?Df$q)({XAx8_2G`vAp?2eGXApPEXY^B?PNsHpfQ4~v3#Rc` zC$Mh)nE=@Xx7Vk^qoYT~)8bgZ4bO}a$(h@^z13xgQgJGe$>8br1u!45?Os>Lz6)pK zG@Jkv3epK0CVC>S_d;O(4*5kQ&Q1 zuDi)m3A;Q}fCK`nI4o8$@=Son&&0Z2|~*jFsEh^!Clw%cnLLu(;TZ2nRTs zihy4}6CHxu0MP*Cq{cA$XFM2$s7HuK`|mFgdPz4s_*>LDIDdO_pz!K43e)cGTd1L) zeQ0|(+P_HC|4;ZSyu|$!bO)gyh@kjK#m3z#Y~JUpo%d=7>#K{`KV83i^Wvo;4P(U;Y@X=-CCqMZFIX*t*V1SY=^TsUa{l&AteU>r zQzx2YXd5NO`&RWK2Xjt{wz!&k?2>-R=bL=isE9|xcaEDj4TO2KxI7oWb0Sg~K=%1m z5jis5PpDY01{lyBsJ{e&f1W6@U^?nOW<|}&(%$%Si_lULM>GHQa2WX&CW5S^RBsjZ zP_3u2+E=KcP-EvUtu{Gt;ouzgi}w8BC%$3}S4Q`x4$|f!95@-dX=kHwtd0weYcZP2 zkj@5-!{FfV4h*mn`~{AIy0B3c;uL2FOOZp)xB+uY-Nd62e87jISs7}!cqydOOCIC$ ztFk&`IqLzLzc4_D7N9#&qDTXoME;``U|7p9Cg=(y0O_y5MV#1Q>9sdaiP_Pxo)454 zQP=}g_kchFtlP&-T`r&VA{vA<3|lhQhIzJ~XmS^EX~Y7#P%}Ar-V&}fMEi-SjS{JW zMWziPy$&Mqt%ur(h;cA3Jh?m5nPB2LxdX-=W5@>44eCe1HyYKbHs|lq2pK{N=k`KJ6d|6)z!9>Ls$pK_R+4FYzCVOABa(3S#kwkU_bjF)| zkOAG5K68x`IV)(538H)QX!cB^*gS3N{A!D=9W7$}NoJYH^z$2KEI&UkVE7%WD6i!- zOIM7HRkVeqV1&J`Nw2k*NFbSH6Rjia#N-INe$kYZJ#2if?@lh*_-U1{1#kB~4Gn&IPvMzVa=uu4` z3}RkosB2SO{%yf)q%Fulpz?ssT-UWF$pud53 zJ}CC7duy~HkoXZ)vnaiYHZjoO6KH{vQJndNl4YXUs;d!N{RfJR#**`fIsPK#_5NPd zz7Py^if@ej{fKqAJPMbB(`_@@FFD#B_rCL48|^UgUGD8@66RhvUIe-KwHLv6-nr;_ z5q#%Te%p&6KLX=LP%t|7BFH~cj3CN``6nUSs>+^{J;-|sqLX$j@NSFk-`pOCkp_3}P~TcAF?U#x zvxTW>Rq}?l zyWBUN9aiutvOTTfS|1aB^zDlbQcnxAVEH#IC~9KK&*ynwJ(#XxebA!gTxRQp)n{aN z%*4&E6t4e(kLMd|aNKK;k`)m!PVBR1>8|>)g-~ZVM*B+XXhmHxXS|-+UMk}O^1DC- z?}xb0Nw=_UjaIOs{YDe1RO?o=pr>w)f!l}%$(^N@zg1WS&i$n#AN0Qsxc?M^D9Ws$ zV__x1Tpi>JZ{w+z0srve=-19w?%vo_3XWVAf); zLK+)haH0!~va1CN*IfDEuI4HWeI;7V&DH-s@#FuRM`yeEzwKNbmH!vv%O*trms-%5 z2!IPC&7-FacPPSzaU{;xl;344MN+%#b4?EXr7CLXl!d;U5G(Frkx;_qF896k^Kufd z?Qr6;MZZF;L^W%~zcqDVeC6rCg5KvDhvi(1;)#-1Jx(_38gye5o4#jYeX0XzxIFZBx%P38459uX#K{Pu1ZN zqhm23*?u7yBsYpo!4xp}lg{%U3zQTSEM(S{H{xWwZSz*U0TnwZ;M&!RRDTg9MF-<} z2;>*_2a~9)-WVTqk;?CgBizUHLH>CJS=S->zP{@d2sTNv!VwXF9t!RP1i zuP)xaeu~9kNtuII;&^l$DP#@N1$P*Z;Ed{mhrjsiO+u^>l~5(r5!EOFFEr@dp7-P~ zhBl}etJX6g=63#62%axWm0U^1njGNOF<`!RiUH@;dGJ=!%5twN?MtXubhu- zrYok(L_hFMe}O-qsS6XVIm>G0MQ~PBr(^S1+TLCUCcslkU8TuH9ZgXx9(LpLAdbkN zG(l55VG)MPOfush#y)8#Wm}!Sf*~4Sy+uSJl5MXL)C`Ld&uPhOW|mBGwCrKU3&%+s z>}8`hAIxy&c*=?E$f@1qN&JvN4+{Hb_SdyZf3 zt)>|#3Ylm7L@2RvU5t8a_LGR?WXMz{DKsE-5Zr2Aq_T7bsHPpK@h8yoU0bh}XfNjV zhV)a=qlI|NOttv1wpeX(>-=(<#M zh8O(L{{+9c0agzlIm`9b0Zcdmm&%z9h({tUXgI{#RWkk=f{Y*)Nbt<=03jl8$Niw@ zzHE;dE_bZYu$s_!C2uIO_%Ga>kzHG(T)tnd=3%!>|No816?y)jI5|a;ZVz6m|E*j6 z`F}0U|I|Fo<$pdoJKNQN-paK;|6hPFRk{942@-{`187L?yvR6iFfG;<+y~+4W~#82 zQWFj0P6`|_^je{m=>YT;RKRF*m$3VInOpZT{A^00S-#wRq~8WE9)sj^+7pN9Vl|*J z`2JbY3I4bG@igcxu)h~n7Oghkc(l3k{&|JrDj`6vc;t^3{)QAF!E4g>jzvbRLJ9rhP9|w_=gp>X%?7#y3-{G;J|Gjy5)ZCr_Te&uM{?Egg>PkS! zr(wJ`dy%3%Nc2d5(G4YK2Xzo!rBQ?)Nl9PZcCJWxQ+GtQa1)InFPu7dj0?M!w1C+N zJJaHg>T}v@bd%x1Fus{agJCiW4~E$3MD_Xi?1TLWW?0lAYY_(dqf%sWlccJtp{2L_ z7 zbz>c+&oCZYp>Cku15g2kBb_ZsO={G=HOUxYq~hue9qPL5YEX^fW!Sxy_7MK+DTXdn z%swVZk^7KxF&--V$mu6D%HhHhN>GHp%&P@4c4o|GY|%1SFPwEjbb=ah{w~ajjb9~4 zG6vb@nK@Qt;8NG9OKfZp&Q$2DGFy{IkOBi0GPgm}frVj}a2jIn9@GNT@)M{PQzETV z4Lrgi+JwC7jo@4(0F{{sZGRtELcEZy-laVpgc2IMw;WQIUuIOw^7JjmGXMa8LOL9a zSAwx+itSrs#pUZFYw`(LlZ)`eS&$N>aF)G%#pxw`(d9dE^aiIzu$czWuo&aZ2}`s%;mzWmGik!m{EIT2re*xQUCqXSk%j282&3Fx1SvzAFQy z^!*D8Jkmw*$3N=HJ=5bWhPMP-tOjsBE-yrYV>7&A^IAxI?4ph@<&C+F2-v^rP~oVL zw@=OEpqE7C5`xkbwxwqC$O2#iTpNRGs4B=7A8ppAoN8o_fxa( zhzCy%O5A&a5kAW+C}Hr*LJczwb#VO^q)FK9+R&dy81;%?1z7Gc8YR=4Tc9hPC67Fd z8g?^4??p3|CZli!MK~JjF#LGX86-CcQj%j|wX)v}M>k3uJ}`SJ%c68rQ_lubf1;?% z_cHb1AZQ)i7IZN`Xn0y6d?>!WO#k}|k|7}|9@_qU)NGyk{NL%(VROg-ZR6UM{@1~m zN_^kJIh`^^Oxs)10fIlMRevPtAkEU;nk4MPEt^qRFyN~Ok@2x(GD;-Q7432tcuUb?h%n{% zYtSAmeD)UWhpe{?)kECJhZqhFSzDRb3yu;5ha2ILY5)#(UO~g^t3BVx#v+N__D(sm z@heaNLp*#A-rSAWa{9Nf1@^zzao+!@b+WVnZRJ{@{^#*!DY2g==%Q0V`jUKX=bFZjhlpblD`Of!7hgbvGNbwJ{ivfyX!Ase~t z=49PvAMI2E4A`Yq&)horUDbm*Qm=DsT)Q4-H&lMFwzpT^GfOJ8ktsq<5CucgJP1Ck z8iZJ&#Nd^gGr%i_l6M(ciMm$Fz?t4I2{L$UlZ+gUu?DMso;89NdOXWil$dXWPf5v) zXisSbsf0@C?i`tT?qq%@>#>6kuuq{@A&p$`Ht3-fd0EB>18L6MZPtELfEhwK$JCJopU35(ol%hd#;_jm13yrKxTS!SUz3wof$Pcw@GSm81is}cVgXni+6BaUjc79H5n!J=R1N`^@B?YkJc@>K$>AjJ)q@;Rw6dmB!(jfemX`2S`rSO5L=q`9;IZRJ{@|1ZRshZO-h-G28r9;j`Jsv%R286xzC zQF;@Ny0ajgBx7hy3k43ZuZYIY4c1NIn8T?eD=CH(F+tiOTaCkDrX>s75YhKKnE=qD z9+$;}dwipm*%l_+um_5$A_HzDh}Vgw^q#q#2QQMEF^V$)w;2Z~>dGcG(K?-HsP?zT z0`iO+Pa#${7!N4TULh|p7?Ee@=T;mf|16Lh;B2zjNN2S#EO>~J^Aqt`s&VM_IEed` z3f;XQjpugg#s$*5%gPingw4{YZo%gTvIBqO1g|X;71-0$kB6kDCA&9E5rL!@7X?j7VCDPn>;TD5Csc0H*je*)dPK(dt6Tnn2kMK&k*^4kQWzVV0{pFs_JbWJv$V{^ogr zR6J?@sV|&1lc}J zJN5tOt~~$Wk13hg+i(Wxaj6&&bx=N-{~y$U_w&CWwNB4=_TQ~s>+}C}@TH;@fCZKg zu;da;1eaky!oSO00A1U0Rf%E}Pt+mk4b;s+q{*ID(hFV;UZO^FOy4SIZwvXP@zFN zV?gONb53+S7>e4{h5G_U`5~473qWE?XH?;l!Zl1g@dQJJFo#--8Wn7X>K#)?8Mc8y zA+8K1rwsgyKzfKUw&qV-okRK^U8pO&RpV6`&L@ zDWvbF7p{TLiKO6>RrlB6jA|Z`!Oi$09;s4`&w?LRO`ilGqp`sUla5A;G*YU6twD5U zrVxh7g}9-jRy2kMgO0^i@gAjsP9RA0zIC z*fUQ1$jPBTLt$&Rn!!aMVAxBh9moho38AJKvaW<@>NQBG3d&IiaHoot8Q1JfArg`5 zXyYQeZXCAfuZJyp$ya36SrjQEmKPsQ{57E_b3OVqX#E{6nfVrcDs=?+kfl)b8LpE5 z?FFr!*u4;9(lfG5Of?U2mfy{JLq@!ecW-93TIqvScLJZ7 zB$t)|uU0Jc#=tNvY*7)L=HZn)f#PrH4A5`SgGF&Uy^PWcWvG$@QkSP;Xc>1YD^yCS zNn$9T&_N^;J_!#(9g2sVA#i!3x{n!#_)F%FHoXp+)lySfR6ae23~V&#D0BK3nPZfU z_Axm`@B=3Of>yZh>8;qTvT$f#49G!AQ%5W7+DI3w?M)RNCF=-kwpQAR-QmS&>RKIyf{K0Mv|ivgq`Z?$qSB>ze|o z;^ZvMdIJ)EsZZ4hStCvk6q64ADA~kFi;dgKaPS{WgdcR2Ah|Dd{~&jyX|1jP#ocBM z(wgPg=ocF_7{(eA_vCS{`sxqzBA>4%`6u%%y?frWvBhit|#tz;_ne$Ljg^faiQ5B&X#gD*xrlJ;=hl=^pgeCx{7 zke1e*h+rH`9P?&W1v{lv#Ov2P*yz}Uo|WQ zyjn7kIbLMKLcrJa$F0@skwD6hi=N8B9N*57 z!OSKBK|a30ywA>s$Q|_3@Gcyb9U;8@dN0VimtS}P9Msu7z0F+4Zo=lxAwN+222# zKU22y^`?=G#F=iMd<5S-lQ(VGCwrRCEBYO+h*Y!uD=;2rg8cGc&ZOhX@Wtwo%DLWqg-}NRa*J=_6!b9_R5>S8||`}+dRaMY;*?+aJI0K=AJdh zo2rzND&An0DM0-a-#KrNrJJo#^ELn5l8vX$nv~Ta&Pd9eqLT};u24WrNNzSJRl1^m z1i8)?I!uxjl)zF~IwJ)rb(-TvXh)ZwCO(pAF!<`l`S_cE%;ON?;d7&vs>+nq++Fgg z93u)TBW<}T*!O#$yE=*tOZi0PQdr~f6P4gUK@&dI=zUej`!|NJKZx*XD4KQD_Ti}IUtLny2NgJmx^5&ZYR z7S?}no*d@l|BjD#_W!M1o2vh#@nxxsKY5o?E4oi~h?YKwg65>)DAa~5JGsd2C{$wgG!V}=g_(?(G65q#J%93cuWia^@m$a0O!e>-DAX`GOTwh3!=#ydZVCCj2=XE5`OHfoDMPsTIUonw$c>;|aE zgLV=fSF7O8Rxa^Hh)0SdM7mkO(GJ#1?5Hh=?EO)cT5yw#785 zU(dQJrdb$=itn3fyPTLG4;#*xl0l)Xrdd9rcIDI1`HeSFbbxCw+;&? z0!vG|Z+K7P8auCWVVJ~y6W`)k|ZB>bhN|>lcQOmJ@RaR$2)m2!uU2+eE101QIm3= zOANO4FxpzDs%}teO!L0u3eeV{z*}vewH15rZ_tarP0Wb+AV}NUbgcGOZflfTh;lC$ zY`EWui2bX+%H)5`y_jUX^WHEXt)>u|FaMvO9OvZ!vsUwDC;xBb+LZiXjxW`c!Cjf} z-`4Va=XFT<_<25QAI0Ipd;`{L?$eXJ0kM22;AnAYk1T_kqm53?97+N8Me%~ORLC;-b zD<$svhP)!W;fOuV`-2dj5EZ&W&vxsWTJNN0;Q*JxDdV3YW3QCN zhcuhi9rdI?p9?`AMCL%vS7%c8`Z+_F!q8XA{g!tucf1T-edT9m=;}$!!)w!AtySP8pZH$P(Ix8zE{-O=FfdxP)Vz9Av{Vo$L#K?{}hXvOk8OYR1_C zl#&>#27B0GcJLql@KciRLtyVdXu9^n@nl~~X8WQD+(+X60pDO>uU{~g(gb2`Z)Ti! zR@{IXtco`gt{)gA$n>IXyWK!R>P&nFNiDyNVLVt5>??kv{?NS4f+GLWgO3wfqelTP>Q;_XvVHySA#B7I?8 zxiBZI6*zpu0WK7V9N&erjDvgC$uNn)lz$U~CxDu2UGa2L_aoZyAb}QCa{wFS#6~H& z*+|qD_$O}xEDLJ7+z%CK)BPBtEj!b}!H0KdH0SUrxZp>>=&7M508IEPj8WIEY+F01 z?L~Tn>fpGc7);X`gHFws<(4PVi{ICPGkHmd8@K(_5U* zo*G-Ad;gnk8?~pV!|4Eh4rG#b3BsqUtpltFA_YVRv2$5fb&TP4IGSB+hcxWUS&MXA*0Zewq%TA}*B_H)6nG5;A*5^2{Grl*ZA&Rc8u9DFs z2A_X&mJ(jvcX92(nSlA9)F~0Gji;!I_i8@Y$DI{4u|m`mO|ciD6G!%@U&D6h>kOPc zyJIZv0|(b6;qw=3{BS6Q!b+MGYklDWH_?*8kGN6r11t9mVjH&Cqf9AubPrWK=$1>g z=iVaLnGQ3bNYeRH5>Qzn-o)y3jIiMI7F0064^yR&~#Q#1**RSZBBma4%adjDRf&6!LlB@sG zI#cMilmE7GZBYIzz?Z7dzk-RdwcM4zV+`Z*xFPw@OL(RMp-KSg=ia}NUIf^|sJ%c^Rk7cNnEop{Q+z0gu zqU&0aKezrYJ|mTuq1{Yz-WC|%+G8r*-jJ^Dv@D~TZ4Id)u39_o<=H6`a>SJYJMs4 z5fJo(rgF7zX&7Ao{O0@xqIjHU0O_!&S!~r_Hb@{<3e1GWr!>Mm(4mS7;DG8W#f zLsPO41xFwaf(uYXTkOdhZ8(kx5wM@Vs1Kw`ub}bp7j?XZ&8e0Ut1vl1jJd_ML%uP& z^?s62?bFL zq+0kGMPuSlRsS#n@XO73`%xyFsE#oW)Swzn3W0gp6=q?JZcgaq@|RUVgkBGQf~k3OY*oTs+?S*g#C*_bM@xNL_}-m~SbZtg5gb4z z>7(ubYo0Cs4O!TTU=?kzI#jluyaeiC<&%P(%Q+Lbpuz8-Y1FC*k34$))MH7UuthbX z^U=(l^UglUql3VX%fWK#l3jLD#vvC5m$ z_jRIC)Q`L1`w8|IR6;3|ho_yC4<_qJ%BpBxl4Al%<-qWww#QR)n_+FA;=~~5BtB6h zSlk2102_+AYJpywcCK#yN{}^<0I0h`7j%~xnwVp2xDF)0vWF5JPeKVXgM^tcz6Bg$ zoN%cQOka&aAYC~$WK@irEw{mwq?)&6+XpwCq*()#b=tagX%J*=vn*|WunrYx?nH_b z6iU+O+HG*oTo)twuqUnkxn!G#HFkCBQV()89R`;ZH+8~!ccS@pxTsSQ67;R-0Mwq% z?-7qguV(aEOij+Cm*{qHE;6!!8yb;h%(`kejlGiMOrx%@|@9wW>fw9KVUI~yFXEDu2X zUb1i!1uE8y-~ylJ7ryqhUs8m$zahZ1MDK1HZejR zt|CynWGxYsD3?Wr??+Dd6>3E1z>mmE5%&$xnsn82l^LNJpKB&2jQS||9pfmb0!)@Q ze&!-n4vo7~@lojS-EEBd+u&^*i77NnhNy=&KcNlfn3q(1HbOw8s93Yfgpa@jEzIAQ zOlgU4{b`EAV)1S_o~=ODs+HPY8Afzkl|jhc!9-@glq`iLcro36p%5o-cNdUJ0-3MvIPiwy2jAq^%x5I<=px1(AYMp^P0 zE&PvzFulP5p*m_6?-Y}8GXkw6WEcV4fQc;V3daOT>dheOfIbFmXMq#joOD!%E5u!6 zva`Aei6)qfD!ApA)Wgf2M&Q?UmC66ZaB_=fc%FyZn(n^~>OY+|A=i^v|Mm3jcqjjF zt@+g9N8K%`jFB&CFO$hq!0yPO-0H43;{`l>(_$1+HzZbe*KE z8~}NIN;IFUxzfl*5o}p}$hkM0Lb;ETW%koO3c%iHx{kph?qDS-%fl2}RmTQ^7{D&g zWVTB$*bo&4QHiVWASCA`21G(N7PQkf*{(P}P&fdyt&HS3nWNQKed#kOZUS=i%7&CP3M`};4+p2A;tGds(GJlWKs5|0!5BBV7O(vW8Z3JT zmq>Xb0Fu?I5P2(YfL;kYXr4G|S+`cu=*xmA@gx8~C2*1BZ*xcN{kd)!=hWsjSiTv;`vQqtXI0Fnm56sH1aZXia zTNZHp_9@kxb4aZ(2&^jy7%^b8NK=KqS(dqZKq+B$jH%AoRO4#q@HA_S7|Tl-d0~R3 z3|I76N5U}pV~ZDRq8ConMX(3%zM3GHmog3xFh2BY@O}h!@X#QthBF{{+2mt5(*b^M zSpaAPmY|K&?=*zq6HZrYi6Z5~qDUludwYx5loBPV4~myHFX!xhLcY$G_y54^ga=qS zk$pj|5{xVPfy|HpI6FPc$N!%+clqD8a;@+Gu>fBxrh&WAALLSf>m+fy)^1U7i0(A+ zS;DdGR5{l2TA+~WTZk@z@Je-(0ueSm1J2Bjz|aJ`9}cnz9QvfDZm5ftj3j8dhJnZI zJRv?MDaFydUG8BluWB+e0h@J1ygTqd{Te|h^bPAY3Zv~is z=d>j|#%W8*_&Bt>gYU9wdj_vr3I$|;Jb{_1XVg=v+owf2vpiy=7kVl`Ur zCwv><-0ly+nV0yUni@|7!_;o3a2lDgDo#RS5`*l4x@r)977yLj=pD4`Y804ajRB|2 z^Y5U@&(H~Km~W;}WmRKS4h{3Wpw>c4O%T(nCrHtFRE+*gm!+S8!fRr(D#9#sNg|=E zE4w;eP@^^JG0$dJ2~Gt_HkI`8)cq8uaX6Y}dnG4e(aJkJE1x93s7kt9oP(Wy)k*-S zT5AU}GpXJhbGwlsKlv05zJiTe=dHjE&#Pn-?ev)k_UE_$3=S*rOARNRHa$WKv* z6;6?Z5Lrehjv4*!u}*HeNSx#!Q)~#xWh8CbeWIv>tEQ<9ARHu1GLj1}1qFsHF#}4r zg2D_~YN^o6RR9%)EbMNWOd}=5M66mOxIlZ(lxkE1#3mtb2Pgx%QCkX^tpXyAK{UFV z+(JG2J&y*exxcek$=Vc!gtcCGE;8_2l z@js_Wt)pH1&)2#(9{(fwGB^I`Ab5WM{_5h*>)&DaHMMgUb;~%TzqI1ZB-2d7K=fIx z8iex&L(c2amQ`(9owPtL6De-0^T~ClF~#j@Gm!)hRSQf~C)nh67(sO>qw3H}9N%2M zd`b~JFb7h-N09CsibDYjfkR161keu8hunv|jE+ypB z@)+cEml!JwaHKa12z8FPvO0yzRaNX!C#CkNbf!*0Z4n7gV&P7yVFYNkes6CHKDi6$ zloBO6E~5!&O*i;+pqL0g@N38>K`CvYNv6k;FsvSQt8$Pwl(Q-4fcy%OzMOU*7}5aB zjyk571!bs2fX*u5hWJQ4W z2`?qMG1!b|chtZ^d7zBmslEsYt1iNooXv?{Y1i)E&mOkyNjh%<4whJeC62p>>eahP z)e;ghPuck&khbUsWqqo{{tp)HJp=4CLw$IgI@77AlQF6oYox3kMByi_?*{XOAwW?m z)mt)EhhcAzGd5|dm)g{b6^}lJ>WQ?8)uYgVq6QfXr$ebU$1RCaDD|1kfu2 z48F1qrj>D>I$b9?cq%luJ1OQmt&^G%wj$cDbhymN-7OwC>}@+KAbrx3P{6fFpESl4 z@YhND;C%cwqw@C&?3|7{-a>c4O4+LZlw zA-+_r1o86ui_7zT6oBjb?}i5;aqEXr%pDwsj+$pj#RcSfOzcR*8X71v2y=a#CZ8a? zt**4Ey?o$Yz)xmDCsF4$I~(nPjAlXY<(u=p00)f`X4C=KIp@I`9~}LLViT-Hi&la~ znm(jQ{Dc+2T>pEwc;OI@!)g|w(*{lgV*#Mpz4ZaO@v60=%#T@cp59>jvQaPiX_REK zFxA0=;~!1u@G4E-_sz1F z(B=ZnL||s(dsg?I%Rr!m;ZxfU2uX>%;4bERb(i>pYBJz2p5+(n!$G&3k5{aThaAov zh9*8~YW8ZC)osVM4Bh~@GMLiI9=eb(9?{ndgq-jH<)l}Ap!!e8%~nDE-{!9V?^dqO z`hVr{W$i4`m7J}dZrfaPeUMTU%b2hVtP>A~$Xiz4Zw8jjn7Mrijo*TM#%7>s`DW}?NSnOM_&=!a zrS4x2l69+pEaLx8T6zBO^km2XZRgq)|L5ULRle_MW}&E3SJpE9jb5{am-bCmQ6&9IG@{@E2*&%A#6A6~!ur1AZzgPYiNb+q z5xKZLY7g}0pactcW(Ck6aWRsj@xypQ_d>&g*Anu^rP#DS)&hs>p%{`i>|jCgKT$7{ zCZrskWp&Gh5x^Z#$wtmX!I58JIl)lDHVD-naulAR3PvI+(A)uPrxZsUNyjfghY-HT z@$oQ$Xo9+|Z(C<3vW-|mnATJHsNHTmi8AE-zHNH&;sZKm2^$I(+i&NqBVJ36H|#=FwrRb=G~-d~)(_v)B3dNv|6oN6o{tR=0b6 zc6=Is+dK@9yW#2KY47++v(xE*#}jgnr0bh#a^Xy(R;ql_b+Gg-tTV|*c-1CrDB;gq z(Tw&Q66Q)R-4_ln%MDKSJ)`K(ed0z7+ruMEji^lHOtx$--$p%n#4Gg_dhnN*tH_%G zR*ZWyeXRxqwYa&FIB=o=5lWTn@o?|789eQJ&<2GP{!5tFCXJ-7>f-DD%+Q(NpluXb zldu>OSq3XsnfW>X><86*6pp;lP{~#P4UN?QhIdNoXPmIu`^0)%(R8>Uj^l&QAn6=*PMT`xbq-s{-LqaRijIy?pFBBfb&tM% z(mZK3kH3w+RXln0M7N;^bA8x2X*5q^-HmY%Oo?{Fr9#u-qr*qOe-K;(wn;P`2haTE zF!Hn)t0`sXJ~?{T@T1=s^hxi3^Ns7Pgs&ew3yuzpM&j5cJJWt&og2uy(vupsxtALR z&AfaR!M^Iz9SF7lfu8>Yh3*mEsZvMn+>RdQkRuW zcP!c?x6$Wt$$j#d=<{Z_N=0=0e7FwAiODxOtd4c`O(E8O|NXPz$(&WP!NYl&sI@Ju z7;#Gd__HEC)akNoSogrT{F*M>d`&fDUXpKurd{)!w$kA*co;m&JvhP#pRETVNb2Fo z>Z#NA(Rb?J_wdjDe(WzWw;4m5_CfT4(D9GK;phG@MVl0s|D<^N?Ig_4xS9+dg->D4 zWMuIuQ=P}%Sgl$?lI7xEp64C&{ND{I^+%8<>)&a%qSbepMvCWL_Yl>D!#cJfrhE=xzJ%2wP>VhL^rZqhrBZ3V1SA$NKo?z+G+>j zC_af08a87~I^&xdoYkQPq&d?J9Ul5J96j2e(W7EP;}|Tz>I?8r948Z4Aj~Gy12?)f znPzskI5C~K3bHwj$AcI%IUo#wP~S!U5Pg&=$C&h`erM4@L8CW}M{zbG@3LVOqU*cd z<%TC=_A!I@Zo^NgCB8|FHygtbiqNWWAzOu2JTGX9_^HZSajUz{Q zNN^BLZ_tB~K7ju1FVx?A4tUSOe`82d1CSaYGUTCuVZv$d6V{)9$)o5!V}ykcIUEr8 z%8*<)viJtn=3(&S{Ed9AFxAYj+yn7gZ5q-S8irR4=Njp~E2H#Nd;?3)q*D#gKtMgZH15=>jXpe=wp+a^%_INPQzBd^e zL~^_a)Ko1F$C=xsenBDni)aweN+idZ!+0{0AtzJVy$U1H>aI}9?8J85 z^OQ@`C-B9EEP0pmY0c$-&|cDufDem%`4ABO4E`<1`HVRMOEQl7ZmF0HFW0-`3VRp; zloVtx?E#GN?%r~c;uMFgK{oKhld9TYyOp{;f@ni@c`KcvRyoAepoQ# zO8ud~n4-MH>5w3fXGedEp5|7nbu|!mAi~LAIF470)6dcOTC=;!p!Z@*O&Gu^E?aJv z4e^2!LC61$GR{+`UDPor3mkO{Lb8**^~5a=wu~gr$2!S3IfVZ3i^u~)X0ti($k{?g zmTblytZeTTwLXUvtyVx9>qP|BPTK4=;84TmK6MCS*{8NXrx%R}$xIy(-?&FLEtb>E zbPW9zPw#q|-CSZ`+}fTuxOtyB$%MU;rO^$t3}$W0<*P#wss3!C@=cU8a=Zoso95wPZQk&o3tx5)z7w3!ruZRMq zRFdc;FpN$*V&p_9pW+M%a+IR80B~OowWp;m?S}@n9VG27GC7DekR3OHkPk7x)(-dD z0pFDbV$v4u^|%MTWFz2ovkwlKo_)`s+nX8P~63%e#zbr7CsUru}!WfAT zVn_;HcQPW^8NHjLo;i6)wvomZlRKF9)S-Z(Qg0PZ)aun$^I>m>_V^c=3))usTLZVE zZr;c%=G^S<33G{V8lU2HGDY2$6)O{rsmlT=pPhXbxRbNPm_&O_zB&H-8AtF^~WJjDp4* z`qoPdDY2;>t?s9~KTX=^$HU5eSr z^?*EbdhY%%OY`o`qB2}^_)!2KOT7CK_kO9KVl2mFJFviNOBmOPIRkvRY06=YI$QR! zqaa7L-D#zT*Ccu5L0zZzbgTrBsEAj#K_+ZLtslt%gji_JAgj#GOxWp= zPIii))h9Mo10vV^l6gTSo}u>ZNwEM;%mu%MIIDd*!oh@P1qLpfD)O7t3hpcbtkG|n z=){_co-A@w;(2>C*!zN-NUJPHC!BO|)hh+dQFit^>+u78pIa0qWoby$)Uw$0Q6CqlT!szVa@etBNv|k?=sI09Sm>nHurvAJofj`T%NY1ZQId-25dL!vHL+z#tXiokLGJ& zP)qLsf2)LyObEq-(6VHqK;uwl;Y^6JwrALIRxpfQ;~7a;{@Eth4lSvdK%5OZ0P-5^ z0XD-RiG^sx{E?`2=Po0?^tIU|k11x&pN~kER@Bj8KNx33E(}-{3`mgmu3ryx4pU^m zibRmx+Z3{`!XJ4Uw-xqi8Fc)Wb4Qy)*BxV2eWR_xAelY7m?tW>hE(6Vn3XFN_a|1Q zQGx{fuzpNbvrMI7gT!Wo3UJggL#SGtd%Tj)yUO9pfs{yliV({h0ALl5t46hoeKoQ@ z+LE3XvZ2#dFT(Ptqpt3CKAefKTp7$}T^&xQl)Kz0Ec2Y;GHP+_i14So3cFEM)wIC5 zr_8BmO=lj5CJTMdt%8?=4f3r~FAezq3)h~-f9`AE;VUbszb;%fNSu=nW@tUO4n)dg zLi}Z3H=I>@Et1 z1gU)(c5h?VADRNsc{67ETVqdEIN-bk2 zqhkrOTeggv?NUUtrZ!2-37;nm!|bL(o(4b+y%N@ih~~qTUwP!~-aNkwZ3L;C0a^sb z^3#yVzquqdW$~W?<#aRQp9|wZ&ziaT&y(iP|9>mjrs6*hzElnWyi%tR&@fCuuf859 zidLwTWP&XrMMA|*=0t^3KW@qfZAtcA30fz?bx*MgkYB|)Eo__W3x1(yT44`D9LU_{ z>8+CjgA#t$SkQ4+*s(;3V-t0mgJtcuQ-1K}bq9xdp?ibw8Unp0Wt~>+;;^{sID0>G z8?OXPKgn$|ATb!KtlvvW@2Gf&hw0h(6tX%N72Cp&=jZh;?RcKYiZzUvpD|K+L&Gre ziU#va-NZyHH)0{1wb~DmKpC4Uiuybcu%7^&q9dy$0-8v%E&`M`x&swH*0t`&z%63P17x+^Gat;D|(h0M;%fQi1Qu*)O zXwbu#=}WBwiigAL1hwqf_?>Ceh8@hp29}!AjB7q1w5Vy(PZhLnHD-D+c3FyU!f^}y z!T%qlaH(xSFwbh6zW}Il+l4VjJaO#hIb9%YL;yYGDL7hwvsBNVnOHAbqf;;0V=dL5$pHttA?(}WQfU?l z_1YqrR9cBbe_cYs#l!KDQ$|>(j<()kG&(!eqc=PUiz~nkK&=D!WCK~drev_z+En!J zEgGz|ElLJEN+ycd2Unnw?Sm}MgM<|Z^`h1gqSuL@{QPm(at;gmt>QaoeE-ayb$+wh zyTaNm_8xL{$hutG5OQD3uBq)cI^h636W;V|ehiyGNd6S7ocY5UB?2qVor;5S`j_r9 zMYjdMH3tprJhrb@?jv{Q`M+_Re4ZVkzJ446_z$kk@AoDv?pt`T5;r)P|2t|P9iI91 zzmA*7hdcgn8`t{$-_rO}F&_vgrj>v7=RRk6TGn_=6cg24QxqPS;=RoW%r8K6Zu=$<+)Aoz;JniR#Ty+kr(b znY)#t!yp@pTDO1ly5TI;OZ(P3;V5VOW7VvZIS)Qqj~H_N=odCf@t+9e7&^U6sE8Nl z=|eEzB*86cZsQUD`;=jO)CexXl&qlM?IM9qFs4d-J0(88H$YqB)2kOuy*p5DfmmVE}(!ZGFps^;- zV9rou%C1dWY>K+52c78z6LHbEIHeJoLW?Gb$xxiRc>|cFG37(Y8Cr3^$*2G@Fg)zA zAI1Y6>P6+g5Kb8>+CiG3>7h4ZU$qSwj)_!gGoiikk(H%9qqHYbmjW|(h3vO#hAL}r z8#EEEG0KWxp=CgciZ-gm98mX}qq8h??AEU=Feg1oGk^VW2=6b8c7e zUg!W!%yd7RA^rx@Ad48!Nt`5F5QV)Eai=$B`7y|*Jw+r5cp3A0>5Q|z`>WAUlc9}K z?vgST-%L}isWwzkdfLlDD)Sc1Bte_-2ulZ2t|3B=FGwXZxxl@q5QsPstv4ML>A1t> z3V(v)fQ2-EG`+K{a{!gO1A!bgbTuyEL0j_W#alq9J=S>P z$wyraFQb^M#$Eg|KKOfjjoo0HwxU%1jJT8D7yvyDr@5dvz+ryb&nr($S-G?zx~ijpDvEb&zg>??c{91Zrk)iCY$DMkgf_ z^ZcM6Ad3%VOV%78b}Z!SQ<%oUT5&lhO?^n(H(5I$V%d377OuW5UMUjPi+qF&G6k)O zAruG%WuKQn!V35^%eR~lSl2?nj3&S0;rc547(vB!c9?bKVN1H#qH)pE?Q_JzkNi^E zvU#t$)8hb`PfE~)%Jg&nWPk5451_g(7q*@T)nt5&PtB%da4PUe`)g5R@2-`vGW~xi z3e~Lx3wM6NJF9pEFZBP-)qgoVJKgF3w{mSt|3433s=0q$m=7L(XBz@cZg9@gUHA&Q zo=DZZ#dd;BYu44hzY9S|{SGJg8iK7+)L)pW`wI;bspvh>%U-0Y^$^^w(BUJmw6$|c zBSR8rCKzMp={0~GaCj44)$)AA*{LD9Vuvy(64zS9T#>OHMjDV)wR#C>!0wTfC~ z(6K=3Y~)4gQ%`x9TUZzhFIhd{h2b;G_X+Xjk*Zm5y;;#QZWMVPJq%8@Rh0IYhut%R zlw{v^IpdNsT}BgKnHfK4zLgooob^G6(MX8rb=(IvNy|8p2*%Rp_qElksU-6 zrll5REvPP6jnanysT9|F?~6Q~X~kzO3f$!HpDp0$YZ^cmVAUBj5ny43c+d@y&=$yD%7GaT>XL z+L)&p47@3&?Bt>ZmR*se-=SZ}uyUyu z4Yd-}RQNJ$K)oa)0)WS2((n6_Q0xX~h6Kx*;zkBgZX64;KtA6%`^aTc(g@jF;I?8q zkfaB7;)d#(-!6DAC>oo5io~wwkGUR1jhhDWW~t(R!Y2 zv4J!J9H@w)IzXn-?fQImCvLrZv|d8C;E1x< z@lZzMXB@eN=irCD20jP6wJO{yQTiSB2?$Wssg{^f1i~e6xH1sLILO5=_{!w6fUl< zj%{TFt}{q_vgQ!v?Akb!RfL^#Qge=fn51**70li#Y-wq0B%>F`)IZ> z=BLquZ*dRsvM&wAF%z9~6WG6Lj+)*2!G6UL@+C64L z-y*V+QBmH6Zyh4<9Xn`dXe)FAPRd_H`(XTt2_RAEGY1Yl<>y;OBCsJrtTBfA{{-{5 zT7=T0EsC9DI9id{Su;qEaBZD>eB{~x6Xbi#GegyZ@${PpffA%Ale%7C*M@=2e-~0s zn+Ei}B{Hpqd5ol>V9@^0uXhxN^LG+4n1AJj`$llyIFXh&B6F#<3n@rbX9-WOcL4~tY&Xn zRp2&!|2SbudYj=Gc5{ua=^RZwolZr~jYNNZV%FthqdwLxqO9`=HFzpGmKeEBeuNRG zSBLP;|M-gSf1_q$GtVo6JN^xCW3y8sm~Q;X+3E8r{`2DO<&OvZe;?QG_J5LJ2E7db z7(N{0fYr@yL7wR63E3H`pFif!hiAW;KcRD>nK$N}>unSen;vvszMn6lge&gCFo0DThvMOoF+V_)U@1~f)f0wku*$%G5 z*6E3UmZca4eYGs&?v)lz8_fiUy6akJY-x|t&OLcxxbCj3R&u{*NhguzJdduyk2D6W4@hmS+w+~5vglHQ%~EG{`QBvcmT&u`c;}how15(6PD;E80u=M6>kw>R6x1*&;)d9bX z2a14krnMC=5f+vuKrE9x{J5A#ksXiKs6jN`|8Be6)Ie5!Etka=KqQV>M){=zj0BG! z?{OGXprp9FM3;1+VkW$jVHQ+)=zD6D zd@)-?^(!2~vn7J65mQP)++Zg84kZABweT+TVwO9iI0EVqUW@)gdC2 z?F|Y;UD(Gvi!}B=8$g1pMzW$%g&0si!qW%}>OcRJRmhH&3X~Wu*v}S}DL-@+JY}e^ k<{#Ph53*-!1u-u5;2Az#hwE@1uK()w6~LVq$^bGN0Ev18UH||9 From c914c8465ffc92dc3d0c8daf39d32679c4ebe5c5 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 21:10:40 -0400 Subject: [PATCH 156/300] Update contracts/token/ERC721/extensions/draft-ERC721Votes.sol Updating constructor Co-authored-by: Francisco Giordano --- contracts/token/ERC721/extensions/draft-ERC721Votes.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 2328c80d131..043e1208508 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -46,7 +46,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { * * It's a good idea to use the same `name` that is defined as the ERC721 token name. */ - constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} + constructor(string memory name) EIP712(name, "1") {} /** * @dev Emitted when an account changes their delegate. From 2a45f363698b4a31edb1be2d326465008be11d2b Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 21:57:59 -0400 Subject: [PATCH 157/300] Including getVotingPower --- contracts/token/ERC721/ERC721.sol | 4 ++-- .../token/ERC721/extensions/draft-ERC721Votes.sol | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index b90f83b4f3f..59585cde564 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -343,7 +343,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { emit Transfer(from, to, tokenId); - _afterTokenTransfer(from, to); + _afterTokenTransfer(from, to, tokenId); } /** @@ -435,5 +435,5 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */ - function _afterTokenTransfer(address from, address to) internal virtual {} + function _afterTokenTransfer(address from, address to, uint256 tokenId) internal virtual {} } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 043e1208508..1b26d7def0a 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -46,7 +46,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { * * It's a good idea to use the same `name` that is defined as the ERC721 token name. */ - constructor(string memory name) EIP712(name, "1") {} + constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} /** * @dev Emitted when an account changes their delegate. @@ -209,7 +209,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { ) internal virtual override{ super._afterTokenTransfer(from, to); - _moveVotingPower(delegates(from), delegates(to), 1); + _moveVotingPower(delegates(from), delegates(to), _getVotingPower()); } /** @@ -289,8 +289,12 @@ abstract contract ERC721Votes is ERC721, EIP712 { /** * @dev Returns token voting power + * The default token value is 1. To implement a different value + * computation you should override it sending the tokenId. */ - function _getVotingPower(uint tokenId) internal virtual returns(uint256) {} + function _getVotingPower() internal virtual returns(uint256) { + return 1; + } function _add(uint256 a, uint256 b) private pure returns (uint256) { return a + b; From deb2b1e4819ef0315a3bde419e7a695fb2549662 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 5 Nov 2021 07:11:59 -0400 Subject: [PATCH 158/300] After prettier run, updating format --- contracts/token/ERC721/ERC721.sol | 6 +++++- .../token/ERC721/extensions/draft-ERC721Votes.sol | 10 +++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 59585cde564..191e83af804 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -435,5 +435,9 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */ - function _afterTokenTransfer(address from, address to, uint256 tokenId) internal virtual {} + function _afterTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual {} } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 1b26d7def0a..d032093aba4 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -288,11 +288,11 @@ abstract contract ERC721Votes is ERC721, EIP712 { } /** - * @dev Returns token voting power - * The default token value is 1. To implement a different value - * computation you should override it sending the tokenId. - */ - function _getVotingPower() internal virtual returns(uint256) { + * @dev Returns token voting power + * The default token value is 1. To implement a different value + * computation you should override it sending the tokenId. + */ + function _getVotingPower() internal virtual returns (uint256) { return 1; } From 6fe31e95ce98e58994cc951a41a931190e7cefe9 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 5 Nov 2021 14:38:12 -0400 Subject: [PATCH 159/300] Renaming totalSupply to totalVotingPower for the code to be cleaner --- .../ERC721/extensions/draft-ERC721Votes.sol | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index d032093aba4..8b842c36a1c 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -32,14 +32,14 @@ abstract contract ERC721Votes is ERC721, EIP712 { uint32 fromBlock; uint224 votes; } - uint256 _totalSupply; + uint256 _totalVotingPower; bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); mapping(address => address) private _delegates; mapping(address => Counters.Counter) private _nonces; mapping(address => Checkpoint[]) private _checkpoints; - Checkpoint[] private _totalSupplyCheckpoints; + Checkpoint[] private _totalVotingPowerCheckpoints; /** * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. @@ -100,16 +100,16 @@ abstract contract ERC721Votes is ERC721, EIP712 { } /** - * @dev Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances. + * @dev Retrieve the `totalVotingPower` at the end of `blockNumber`. Note, this value is the sum of all balances. * It is but NOT the sum of all the delegated votes! * * Requirements: * * - `blockNumber` must have been already mined */ - function getPastTotalSupply(uint256 blockNumber) public view returns (uint256) { + function getPastVotingPower(uint256 blockNumber) public view returns (uint256) { require(blockNumber < block.number, "ERC721Votes: block not yet mined"); - return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber); + return _checkpointsLookup(_totalVotingPowerCheckpoints, blockNumber); } /** @@ -181,12 +181,12 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been increased. */ function _mint(address account, uint256 tokenId) internal virtual override { - require(_totalSupply + 1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + require(_totalVotingPower + 1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); super._mint(account, tokenId); - _totalSupply += 1; + _totalVotingPower += 1; - _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); + _writeCheckpoint(_totalVotingPowerCheckpoints, _add, 1); } /** @@ -194,8 +194,8 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); - _totalSupply -= 1; - _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); + _totalVotingPower -= 1; + _writeCheckpoint(_totalVotingPowerCheckpoints, _subtract, 1); } /** @@ -209,7 +209,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { ) internal virtual override{ super._afterTokenTransfer(from, to); - _moveVotingPower(delegates(from), delegates(to), _getVotingPower()); + _moveVotingPower(delegates(from), delegates(to), 1); } /** @@ -287,15 +287,6 @@ abstract contract ERC721Votes is ERC721, EIP712 { return _domainSeparatorV4(); } - /** - * @dev Returns token voting power - * The default token value is 1. To implement a different value - * computation you should override it sending the tokenId. - */ - function _getVotingPower() internal virtual returns (uint256) { - return 1; - } - function _add(uint256 a, uint256 b) private pure returns (uint256) { return a + b; } From 51ade3f43f77671d10e8617377ff657fe3c1f9cb Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 25 Nov 2021 09:41:19 -0400 Subject: [PATCH 160/300] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e582c71bdd4..67b0e9494ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ## 4.4.0 (2021-11-25) +## Unreleased + * `Voting`: Create library to be use for ERC721 and ERC1155 voting ([#2944](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2944)) * `Ownable`: add an internal `_transferOwnership(address)`. ([#2568](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2568)) * `AccessControl`: add internal `_grantRole(bytes32,address)` and `_revokeRole(bytes32,address)`. ([#2568](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2568)) * `AccessControl`: mark `_setupRole(bytes32,address)` as deprecated in favor of `_grantRole(bytes32,address)`. ([#2568](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2568)) From 3aec0a7146484d5d2fdf0f19e14585d0d9b28c57 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 25 Nov 2021 09:54:14 -0400 Subject: [PATCH 161/300] Create Checkpoints and Voting libraries --- contracts/utils/Checkpoints.sol | 67 +++++++++++++++++ contracts/utils/Voting.sol | 126 ++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 contracts/utils/Checkpoints.sol create mode 100644 contracts/utils/Voting.sol diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol new file mode 100644 index 00000000000..408f4fc0d35 --- /dev/null +++ b/contracts/utils/Checkpoints.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./math/Math.sol"; +import "./math/SafeCast.sol"; + +library Checkpoints { + struct Checkpoint { + uint32 index; + uint224 value; + } + + struct History { + Checkpoint[] _checkpoints; + } + + function length(History storage self) internal view returns (uint256) { + return self._checkpoints.length; + } + + function at(History storage self, uint256 pos) internal view returns (Checkpoint memory) { + return self._checkpoints[pos]; + } + + function latest(History storage self) internal view returns (uint256) { + uint256 pos = length(self); + return pos == 0 ? 0 : at(self, pos - 1).value; + } + + function past(History storage self, uint256 index) internal view returns (uint256) { + require(index < block.number, "block not yet mined"); + + uint256 high = length(self); + uint256 low = 0; + while (low < high) { + uint256 mid = Math.average(low, high); + if (at(self, mid).index > index) { + high = mid; + } else { + low = mid + 1; + } + } + return high == 0 ? 0 : at(self, high - 1).value; + } + + function push( + History storage self, + uint256 value + ) internal returns (uint256, uint256) { + uint256 pos = length(self); + uint256 old = latest(self); + if (pos > 0 && self._checkpoints[pos - 1].index == block.number) { + self._checkpoints[pos - 1].value = SafeCast.toUint224(value); + } else { + self._checkpoints.push(Checkpoint({ index: SafeCast.toUint32(block.number), value: SafeCast.toUint224(value) })); + } + return (old, value); + } + + function push( + History storage self, + function(uint256, uint256) view returns (uint256) op, + uint256 delta + ) internal returns (uint256, uint256) { + return push(self, op(latest(self), delta)); + } +} diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol new file mode 100644 index 00000000000..b834e239a8f --- /dev/null +++ b/contracts/utils/Voting.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./Checkpoints.sol"; + +library Voting { + using Checkpoints for Checkpoints.History; + + struct Votes { + mapping(address => address) _delegation; + mapping(address => Checkpoints.History) _userCheckpoints; + Checkpoints.History _totalCheckpoints; + } + + function getVotes(Votes storage self, address account) internal view returns (uint256) { + return self._userCheckpoints[account].latest(); + } + + function getVotesAt(Votes storage self, address account, uint256 timestamp) internal view returns (uint256) { + return self._userCheckpoints[account].past(timestamp); + } + + function getTotalVotes(Votes storage self) internal view returns (uint256) { + return self._totalCheckpoints.latest(); + } + + function getTotalVotesAt(Votes storage self, uint256 timestamp) internal view returns (uint256) { + return self._totalCheckpoints.past(timestamp); + } + + function delegates(Votes storage self, address account) internal view returns (address) { + return self._delegation[account]; + } + + function delegate(Votes storage self, address account, address newDelegation, uint256 balance) internal { + address oldDelegation = delegates(self, account); + self._delegation[account] = newDelegation; + moveVotingPower(self, oldDelegation, newDelegation, balance, _dummy); + } + + function delegate( + Votes storage self, + address account, + address newDelegation, + uint256 balance, + function(address, uint256, uint256) hookDelegateVotesChanged + ) internal { + address oldDelegation = delegates(self, account); + self._delegation[account] = newDelegation; + moveVotingPower(self, oldDelegation, newDelegation, balance, hookDelegateVotesChanged); + } + + function mint(Votes storage self, address to, uint256 amount) internal { + self._totalCheckpoints.push(_add, amount); + moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); + } + + function mint( + Votes storage self, + address to, + uint256 amount, + function(address, uint256, uint256) hookDelegateVotesChanged + ) internal { + self._totalCheckpoints.push(_add, amount); + moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); + } + + function burn(Votes storage self, address from, uint256 amount) internal { + self._totalCheckpoints.push(_subtract, amount); + moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); + } + + function burn( + Votes storage self, + address from, + uint256 amount, + function(address, uint256, uint256) hookDelegateVotesChanged + ) internal { + self._totalCheckpoints.push(_subtract, amount); + moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); + } + + function transfer(Votes storage self, address from, address to, uint256 amount) internal { + moveVotingPower(self, delegates(self, from), delegates(self, to), amount, _dummy); + } + + function transfer( + Votes storage self, + address from, + address to, + uint256 amount, + function(address, uint256, uint256) hookDelegateVotesChanged + ) internal { + moveVotingPower(self, delegates(self, from), delegates(self, to), amount, hookDelegateVotesChanged); + } + + function _moveVotingPower( + Votes storage self, + address src, + address dst, + uint256 amount, + function(address, uint256, uint256) hookDelegateVotesChanged + ) private { + if (src != dst && amount > 0) { + if (src != address(0)) { + (uint256 oldValue, uint256 newValue) = self._userCheckpoints[src].push(_subtract, amount); + hookDelegateVotesChanged(src, oldValue, newValue); + } + if (dst != address(0)) { + (uint256 oldValue, uint256 newValue) = self._userCheckpoints[dst].push(_add, amount); + hookDelegateVotesChanged(dst, oldValue, newValue); + } + } + } + + function _add(uint256 a, uint256 b) private pure returns (uint256) { + return a + b; + } + + function _subtract(uint256 a, uint256 b) private pure returns (uint256) { + return a - b; + } + + function _dummy(address, uint256, uint256) private pure {} + +} From 41aa303e399c14a448bc36d4aa3c80df02aefc6b Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 25 Nov 2021 16:40:27 -0400 Subject: [PATCH 162/300] Update ERC721Votes contract to use library --- .../ERC721/extensions/draft-ERC721Votes.sol | 113 +++--------------- contracts/utils/Voting.sol | 16 +-- 2 files changed, 25 insertions(+), 104 deletions(-) diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 8b842c36a1c..079d9dae64a 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import "../ERC721.sol"; +import "../../../utils/Voting.sol"; import "../../../utils/Counters.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; @@ -27,19 +28,19 @@ import "../../../utils/cryptography/draft-EIP712.sol"; */ abstract contract ERC721Votes is ERC721, EIP712 { using Counters for Counters.Counter; + using Voting for Voting.Votes; struct Checkpoint { uint32 fromBlock; uint224 votes; } + uint256 _totalVotingPower; bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); - mapping(address => address) private _delegates; + Voting.Votes private _votes; mapping(address => Counters.Counter) private _nonces; - mapping(address => Checkpoint[]) private _checkpoints; - Checkpoint[] private _totalVotingPowerCheckpoints; /** * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. @@ -58,33 +59,25 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); - /** - * @dev Get the `pos`-th checkpoint for `account`. - */ - function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoint memory) { - return _checkpoints[account][pos]; - } - /** * @dev Get number of checkpoints for `account`. */ function numCheckpoints(address account) public view virtual returns (uint32) { - return SafeCast.toUint32(_checkpoints[account].length); + return SafeCast.toUint32(_votes.getVotes(account)); } /** * @dev Get the address `account` is currently delegating to. */ function delegates(address account) public view virtual returns (address) { - return _delegates[account]; + return _votes.delegates(account); } /** * @dev Gets the current votes balance for `account` */ function getVotes(address account) public view returns (uint256) { - uint256 pos = _checkpoints[account].length; - return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes; + return _votes.getVotes(account); } /** @@ -95,8 +88,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { * - `blockNumber` must have been already mined */ function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) { - require(blockNumber < block.number, "ERC721Votes: block not yet mined"); - return _checkpointsLookup(_checkpoints[account], blockNumber); + return _votes.getVotesAt(account, blockNumber); } /** @@ -109,38 +101,9 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function getPastVotingPower(uint256 blockNumber) public view returns (uint256) { require(blockNumber < block.number, "ERC721Votes: block not yet mined"); - return _checkpointsLookup(_totalVotingPowerCheckpoints, blockNumber); - } - - /** - * @dev Lookup a value in a list of (sorted) checkpoints. - */ - function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) private view returns (uint256) { - // We run a binary search to look for the earliest checkpoint taken after `blockNumber`. - // - // During the loop, the index of the wanted checkpoint remains in the range [low-1, high). - // With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant. - // - If the middle checkpoint is after `blockNumber`, we look in [low, mid) - // - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high) - // Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not - // out of bounds (in which case we're looking too far in the past and the result is 0). - // Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is - // past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out - // the same. - uint256 high = ckpts.length; - uint256 low = 0; - while (low < high) { - uint256 mid = Math.average(low, high); - if (ckpts[mid].fromBlock > blockNumber) { - high = mid; - } else { - low = mid + 1; - } - } - - return high == 0 ? 0 : ckpts[high - 1].votes; + return _votes.getTotalVotesAt(blockNumber); } - + /** * @dev Delegate votes from the sender to `delegatee`. */ @@ -186,7 +149,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { super._mint(account, tokenId); _totalVotingPower += 1; - _writeCheckpoint(_totalVotingPowerCheckpoints, _add, 1); + _votes.mint(account, 1, _hookDelegateVotesChanged); } /** @@ -195,7 +158,8 @@ abstract contract ERC721Votes is ERC721, EIP712 { function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); _totalVotingPower -= 1; - _writeCheckpoint(_totalVotingPowerCheckpoints, _subtract, 1); + address from = ownerOf(tokenId); + _votes.burn(from, 1, _hookDelegateVotesChanged); } /** @@ -218,47 +182,8 @@ abstract contract ERC721Votes is ERC721, EIP712 { * Emits events {DelegateChanged} and {DelegateVotesChanged}. */ function _delegate(address delegator, address delegatee) internal virtual { - address currentDelegate = delegates(delegator); - uint256 delegatorBalance = balanceOf(delegator); - _delegates[delegator] = delegatee; - - emit DelegateChanged(delegator, currentDelegate, delegatee); - - _moveVotingPower(currentDelegate, delegatee, delegatorBalance); - } - - function _moveVotingPower( - address src, - address dst, - uint256 amount - ) private { - if (src != dst && amount > 0) { - if (src != address(0)) { - (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[src], _subtract, amount); - emit DelegateVotesChanged(src, oldWeight, newWeight); - } - - if (dst != address(0)) { - (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[dst], _add, amount); - emit DelegateVotesChanged(dst, oldWeight, newWeight); - } - } - } - - function _writeCheckpoint( - Checkpoint[] storage ckpts, - function(uint256, uint256) view returns (uint256) op, - uint256 delta - ) private returns (uint256 oldWeight, uint256 newWeight) { - uint256 pos = ckpts.length; - oldWeight = pos == 0 ? 0 : ckpts[pos - 1].votes; - newWeight = op(oldWeight, delta); - - if (pos > 0 && ckpts[pos - 1].fromBlock == block.number) { - ckpts[pos - 1].votes = SafeCast.toUint224(newWeight); - } else { - ckpts.push(Checkpoint({fromBlock: SafeCast.toUint32(block.number), votes: SafeCast.toUint224(newWeight)})); - } + emit DelegateChanged(delegator, delegates(delegator), delegatee); + _votes.delegate(delegator, delegatee, balanceOf(delegator), _hookDelegateVotesChanged); } /** @@ -287,11 +212,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { return _domainSeparatorV4(); } - function _add(uint256 a, uint256 b) private pure returns (uint256) { - return a + b; - } - - function _subtract(uint256 a, uint256 b) private pure returns (uint256) { - return a - b; + function _hookDelegateVotesChanged(address account, uint256 previousBalance, uint256 newBalance) private { + emit DelegateVotesChanged(account, previousBalance, newBalance); } } diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol index b834e239a8f..e81e555798e 100644 --- a/contracts/utils/Voting.sol +++ b/contracts/utils/Voting.sol @@ -35,7 +35,7 @@ library Voting { function delegate(Votes storage self, address account, address newDelegation, uint256 balance) internal { address oldDelegation = delegates(self, account); self._delegation[account] = newDelegation; - moveVotingPower(self, oldDelegation, newDelegation, balance, _dummy); + _moveVotingPower(self, oldDelegation, newDelegation, balance, _dummy); } function delegate( @@ -47,12 +47,12 @@ library Voting { ) internal { address oldDelegation = delegates(self, account); self._delegation[account] = newDelegation; - moveVotingPower(self, oldDelegation, newDelegation, balance, hookDelegateVotesChanged); + _moveVotingPower(self, oldDelegation, newDelegation, balance, hookDelegateVotesChanged); } function mint(Votes storage self, address to, uint256 amount) internal { self._totalCheckpoints.push(_add, amount); - moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); + _moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); } function mint( @@ -62,12 +62,12 @@ library Voting { function(address, uint256, uint256) hookDelegateVotesChanged ) internal { self._totalCheckpoints.push(_add, amount); - moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); + _moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); } function burn(Votes storage self, address from, uint256 amount) internal { self._totalCheckpoints.push(_subtract, amount); - moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); + _moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); } function burn( @@ -77,11 +77,11 @@ library Voting { function(address, uint256, uint256) hookDelegateVotesChanged ) internal { self._totalCheckpoints.push(_subtract, amount); - moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); + _moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); } function transfer(Votes storage self, address from, address to, uint256 amount) internal { - moveVotingPower(self, delegates(self, from), delegates(self, to), amount, _dummy); + _moveVotingPower(self, delegates(self, from), delegates(self, to), amount, _dummy); } function transfer( @@ -91,7 +91,7 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) internal { - moveVotingPower(self, delegates(self, from), delegates(self, to), amount, hookDelegateVotesChanged); + _moveVotingPower(self, delegates(self, from), delegates(self, to), amount, hookDelegateVotesChanged); } function _moveVotingPower( From 4d1912bf51f9d233f9102f55ec178b53cd4923be Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 09:22:58 -0400 Subject: [PATCH 163/300] Add function to Voting library --- contracts/utils/Voting.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol index e81e555798e..963f28d17c2 100644 --- a/contracts/utils/Voting.sol +++ b/contracts/utils/Voting.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import "./Checkpoints.sol"; +import "hardhat/console.sol"; library Voting { using Checkpoints for Checkpoints.History; @@ -23,6 +24,10 @@ library Voting { function getTotalVotes(Votes storage self) internal view returns (uint256) { return self._totalCheckpoints.latest(); } + + function getTotalAccountVotes(Votes storage self, address account) internal view returns (uint256) { + return self._userCheckpoints[account].length(); + } function getTotalVotesAt(Votes storage self, uint256 timestamp) internal view returns (uint256) { return self._totalCheckpoints.past(timestamp); @@ -51,6 +56,7 @@ library Voting { } function mint(Votes storage self, address to, uint256 amount) internal { + console.log("minting 1"); self._totalCheckpoints.push(_add, amount); _moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); } @@ -61,11 +67,13 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) internal { + console.log("minting 2"); self._totalCheckpoints.push(_add, amount); _moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); } function burn(Votes storage self, address from, uint256 amount) internal { + console.log("burn 1"); self._totalCheckpoints.push(_subtract, amount); _moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); } @@ -76,6 +84,7 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) internal { + console.log("burn 2"); self._totalCheckpoints.push(_subtract, amount); _moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); } @@ -101,6 +110,8 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) private { + console.log("moving voting power"); + console.log(block.number); if (src != dst && amount > 0) { if (src != address(0)) { (uint256 oldValue, uint256 newValue) = self._userCheckpoints[src].push(_subtract, amount); From 2524a87f30ed10b74ea6262f704e95dff59a0b8c Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 09:53:22 -0400 Subject: [PATCH 164/300] Update ERC721Governance contracts --- .../ERC721/extensions/draft-ERC721Votes.sol | 19 +++++++++++-------- contracts/utils/Voting.sol | 11 ++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 079d9dae64a..21da3a4975d 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -7,6 +7,7 @@ import "../ERC721.sol"; import "../../../utils/Voting.sol"; import "../../../utils/Counters.sol"; import "../../../utils/math/Math.sol"; +import "../../../utils/Checkpoints.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; import "../../../utils/cryptography/draft-EIP712.sol"; @@ -30,11 +31,6 @@ abstract contract ERC721Votes is ERC721, EIP712 { using Counters for Counters.Counter; using Voting for Voting.Votes; - struct Checkpoint { - uint32 fromBlock; - uint224 votes; - } - uint256 _totalVotingPower; bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); @@ -63,7 +59,14 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Get number of checkpoints for `account`. */ function numCheckpoints(address account) public view virtual returns (uint32) { - return SafeCast.toUint32(_votes.getVotes(account)); + return SafeCast.toUint32(_votes.getTotalAccountVotes(account)); + } + + /** + * @dev Get the `pos`-th checkpoint for `account`. + */ + function checkpointAt(address account, uint32 pos) public view virtual returns (Checkpoints.Checkpoint memory) { + return _votes.getTotalAccountVotesAt(account, pos); } /** @@ -156,9 +159,9 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been decreased. */ function _burn(uint256 tokenId) internal virtual override { - super._burn(tokenId); - _totalVotingPower -= 1; address from = ownerOf(tokenId); + super._burn(tokenId); + _totalVotingPower -= 1; _votes.burn(from, 1, _hookDelegateVotesChanged); } diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol index 963f28d17c2..86541164b32 100644 --- a/contracts/utils/Voting.sol +++ b/contracts/utils/Voting.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.0; import "./Checkpoints.sol"; -import "hardhat/console.sol"; library Voting { using Checkpoints for Checkpoints.History; @@ -29,6 +28,10 @@ library Voting { return self._userCheckpoints[account].length(); } + function getTotalAccountVotesAt(Votes storage self, address account, uint32 pos) internal view returns (Checkpoints.Checkpoint memory) { + return self._userCheckpoints[account].at(pos); + } + function getTotalVotesAt(Votes storage self, uint256 timestamp) internal view returns (uint256) { return self._totalCheckpoints.past(timestamp); } @@ -56,7 +59,6 @@ library Voting { } function mint(Votes storage self, address to, uint256 amount) internal { - console.log("minting 1"); self._totalCheckpoints.push(_add, amount); _moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); } @@ -67,13 +69,11 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) internal { - console.log("minting 2"); self._totalCheckpoints.push(_add, amount); _moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); } function burn(Votes storage self, address from, uint256 amount) internal { - console.log("burn 1"); self._totalCheckpoints.push(_subtract, amount); _moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); } @@ -84,7 +84,6 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) internal { - console.log("burn 2"); self._totalCheckpoints.push(_subtract, amount); _moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); } @@ -110,8 +109,6 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) private { - console.log("moving voting power"); - console.log(block.number); if (src != dst && amount > 0) { if (src != address(0)) { (uint256 oldValue, uint256 newValue) = self._userCheckpoints[src].push(_subtract, amount); From 1e46d257f13194e0d49c0b2b146723d539bab8a3 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 11:30:11 -0400 Subject: [PATCH 165/300] Documentation for Checkpoints library --- contracts/utils/Checkpoints.sol | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index 408f4fc0d35..3e34f1f6933 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -4,6 +4,9 @@ pragma solidity ^0.8.0; import "./math/Math.sol"; import "./math/SafeCast.sol"; +/** + * @dev Checkpoints operations. + */ library Checkpoints { struct Checkpoint { uint32 index; @@ -14,19 +17,31 @@ library Checkpoints { Checkpoint[] _checkpoints; } + /** + * @dev Returns checkpoints length. + */ function length(History storage self) internal view returns (uint256) { return self._checkpoints.length; } + /** + * @dev Returns checkpoints at given position. + */ function at(History storage self, uint256 pos) internal view returns (Checkpoint memory) { return self._checkpoints[pos]; } + /** + * @dev Returns total amount of checkpoints. + */ function latest(History storage self) internal view returns (uint256) { uint256 pos = length(self); return pos == 0 ? 0 : at(self, pos - 1).value; } + /** + * @dev Returns checkpoints at given block number. + */ function past(History storage self, uint256 index) internal view returns (uint256) { require(index < block.number, "block not yet mined"); @@ -43,6 +58,9 @@ library Checkpoints { return high == 0 ? 0 : at(self, high - 1).value; } + /** + * @dev Creates checkpoint + */ function push( History storage self, uint256 value @@ -57,6 +75,9 @@ library Checkpoints { return (old, value); } + /** + * @dev Creates checkpoint + */ function push( History storage self, function(uint256, uint256) view returns (uint256) op, From 7d4c5fed3e055d10b70e5fff231a4eff59767e6a Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 11:37:12 -0400 Subject: [PATCH 166/300] Add documentation for Voting library --- contracts/utils/Voting.sol | 65 +++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol index 86541164b32..caec09662f8 100644 --- a/contracts/utils/Voting.sol +++ b/contracts/utils/Voting.sol @@ -3,6 +3,9 @@ pragma solidity ^0.8.0; import "./Checkpoints.sol"; +/** + * @dev Voting operations. + */ library Voting { using Checkpoints for Checkpoints.History; @@ -12,40 +15,67 @@ library Voting { Checkpoints.History _totalCheckpoints; } + /** + * @dev Returns total amount of votes for account. + */ function getVotes(Votes storage self, address account) internal view returns (uint256) { return self._userCheckpoints[account].latest(); } + /** + * @dev Returns total amount of votes at given position. + */ function getVotesAt(Votes storage self, address account, uint256 timestamp) internal view returns (uint256) { return self._userCheckpoints[account].past(timestamp); } + /** + * @dev Get checkpoint for `account` for specific position. + */ + function getTotalAccountVotesAt(Votes storage self, address account, uint32 pos) internal view returns (Checkpoints.Checkpoint memory) { + return self._userCheckpoints[account].at(pos); + } + + /** + * @dev Returns total amount of votes. + */ function getTotalVotes(Votes storage self) internal view returns (uint256) { return self._totalCheckpoints.latest(); } + /** + * @dev Get number of checkpoints for `account`. + */ function getTotalAccountVotes(Votes storage self, address account) internal view returns (uint256) { return self._userCheckpoints[account].length(); } - function getTotalAccountVotesAt(Votes storage self, address account, uint32 pos) internal view returns (Checkpoints.Checkpoint memory) { - return self._userCheckpoints[account].at(pos); - } - + /** + * @dev Returns all votes for timestamp. + */ function getTotalVotesAt(Votes storage self, uint256 timestamp) internal view returns (uint256) { return self._totalCheckpoints.past(timestamp); } + /** + * @dev Updates delegation information. + */ function delegates(Votes storage self, address account) internal view returns (address) { return self._delegation[account]; } + /** + * @dev Delegates voting power. + */ function delegate(Votes storage self, address account, address newDelegation, uint256 balance) internal { address oldDelegation = delegates(self, account); self._delegation[account] = newDelegation; _moveVotingPower(self, oldDelegation, newDelegation, balance, _dummy); } + /** + * @dev Delegates voting power. + */ function delegate( Votes storage self, address account, @@ -58,11 +88,17 @@ library Voting { _moveVotingPower(self, oldDelegation, newDelegation, balance, hookDelegateVotesChanged); } + /** + * @dev Mints new vote. + */ function mint(Votes storage self, address to, uint256 amount) internal { self._totalCheckpoints.push(_add, amount); _moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); } + /** + * @dev Mints new vote. + */ function mint( Votes storage self, address to, @@ -73,11 +109,17 @@ library Voting { _moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); } + /** + * @dev Burns new vote. + */ function burn(Votes storage self, address from, uint256 amount) internal { self._totalCheckpoints.push(_subtract, amount); _moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); } + /** + * @dev Burns new vote. + */ function burn( Votes storage self, address from, @@ -88,10 +130,16 @@ library Voting { _moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); } + /** + * @dev Transfers voting power. + */ function transfer(Votes storage self, address from, address to, uint256 amount) internal { _moveVotingPower(self, delegates(self, from), delegates(self, to), amount, _dummy); } + /** + * @dev Transfers voting power. + */ function transfer( Votes storage self, address from, @@ -102,6 +150,9 @@ library Voting { _moveVotingPower(self, delegates(self, from), delegates(self, to), amount, hookDelegateVotesChanged); } + /** + * @dev Moves voting power. + */ function _moveVotingPower( Votes storage self, address src, @@ -121,10 +172,16 @@ library Voting { } } + /** + * @dev Adds two numbers. + */ function _add(uint256 a, uint256 b) private pure returns (uint256) { return a + b; } + /** + * @dev Subtracts two numbers. + */ function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } From 44df89dd68cb5754712095e2e6aca11b0e0ed02e Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 12:06:34 -0400 Subject: [PATCH 167/300] Add Mock contracts for Voting and checkpoints test --- contracts/mocks/CheckpointsImpl.sol | 31 ++++++++++++++++ contracts/mocks/VotingImpl.sol | 56 +++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 contracts/mocks/CheckpointsImpl.sol create mode 100644 contracts/mocks/VotingImpl.sol diff --git a/contracts/mocks/CheckpointsImpl.sol b/contracts/mocks/CheckpointsImpl.sol new file mode 100644 index 00000000000..e5e0f92cd6c --- /dev/null +++ b/contracts/mocks/CheckpointsImpl.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/Checkpoints.sol"; + +contract CheckpointsImpl { + using Checkpoints for Checkpoints.History; + + Checkpoints.History private _totalCheckpoints; + + function length() public view returns (uint256) { + return _totalCheckpoints.length(); + } + + function at(uint256 pos) public view returns (Checkpoints.Checkpoint memory) { + return _totalCheckpoints.at(pos); + } + + function latest() public view returns (uint256) { + return _totalCheckpoints.latest(); + } + + function past(uint256 index) public view returns (uint256) { + return _totalCheckpoints.past(index); + } + + function push(uint256 value) public returns (uint256, uint256) { + return _totalCheckpoints.push(value); + } +} diff --git a/contracts/mocks/VotingImpl.sol b/contracts/mocks/VotingImpl.sol new file mode 100644 index 00000000000..6dd7990b97c --- /dev/null +++ b/contracts/mocks/VotingImpl.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/Voting.sol"; + +contract VotingImpl { + using Voting for Voting.Votes; + + Voting.Votes private _votes; + + function getVotes(address account) public view returns (uint256) { + return _votes.getVotes(account); + } + + function getVotesAt(address account, uint256 timestamp) public view returns (uint256) { + return _votes.getVotesAt(account, timestamp); + } + + function getTotalAccountVotesAt(address account, uint32 pos) public view returns (Checkpoints.Checkpoint memory) { + return _votes.getTotalAccountVotesAt(account, pos); + } + + function getTotalVotes() public view returns (uint256) { + return _votes.getTotalVotes(); + } + + function getTotalAccountVotes(address account) public view returns (uint256) { + return _votes.getTotalAccountVotes(account); + } + + function getTotalVotesAt(uint256 timestamp) public view returns (uint256) { + return _votes.getTotalVotesAt(timestamp); + } + + function delegates(address account) public view returns (address) { + return _votes.delegates(account); + } + + function delegate(address account, address newDelegation, uint256 balance) public { + return _votes.delegate(account, newDelegation, balance); + } + + function mint(address to, uint256 amount) public { + return _votes.mint(to, amount); + } + + function burn(address from, uint256 amount) public { + return _votes.burn(from, amount); + } + + function transfer(address from, address to, uint256 amount) public { + return _votes.transfer(from, to, amount); + } + +} From 55213f562fddf53dcd27f454418ae103f494c4af Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 15:03:10 -0400 Subject: [PATCH 168/300] Add Voting library tests --- contracts/utils/Voting.sol | 4 +-- test/utils/Voting.test.js | 59 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 test/utils/Voting.test.js diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol index caec09662f8..0f33096c92c 100644 --- a/contracts/utils/Voting.sol +++ b/contracts/utils/Voting.sol @@ -35,7 +35,7 @@ library Voting { function getTotalAccountVotesAt(Votes storage self, address account, uint32 pos) internal view returns (Checkpoints.Checkpoint memory) { return self._userCheckpoints[account].at(pos); } - + /** * @dev Returns total amount of votes. */ @@ -58,7 +58,7 @@ library Voting { } /** - * @dev Updates delegation information. + * @dev Returns account delegation. */ function delegates(Votes storage self, address account) internal view returns (address) { return self._delegation[account]; diff --git a/test/utils/Voting.test.js b/test/utils/Voting.test.js new file mode 100644 index 00000000000..7f883886179 --- /dev/null +++ b/test/utils/Voting.test.js @@ -0,0 +1,59 @@ +const { expectRevert } = require('@openzeppelin/test-helpers'); + +const { expect } = require('chai'); + +const VotingImp = artifacts.require('VotingImpl'); + +contract('Voting', function (accounts) { + const [ account1, account2, account3 ] = accounts; + beforeEach(async function () { + this.voting = await VotingImp.new(); + }); + + it('starts with zero votes', async function () { + expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('0'); + }); + + describe('move voting power', function () { + + beforeEach(async function () { + this.tx1 = await this.voting.mint(account1,1); + this.tx2 = await this.voting.mint(account2,1); + this.tx3 = await this.voting.mint(account3,1); + }); + + it('mints', async function () { + expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('3'); + + expect(await this.voting.getTotalVotesAt(this.tx1.receipt.blockNumber-1)).to.be.bignumber.equal('0'); + expect(await this.voting.getTotalVotesAt(this.tx2.receipt.blockNumber-1)).to.be.bignumber.equal('1'); + expect(await this.voting.getTotalVotesAt(this.tx3.receipt.blockNumber-1)).to.be.bignumber.equal('2'); + }); + + it('burns', async function () { + await this.voting.burn(account1,1); + expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('2'); + + await this.voting.burn(account2,1); + expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('1'); + + await this.voting.burn(account3,1); + expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('0'); + }); + + it('delegates', async function () { + await this.voting.delegate(account3, account2, 1); + + expect(await this.voting.delegates(account3)).to.be.equal(account2); + }); + + it('transfers', async function () { + await this.voting.delegate(account1, account2, 1); + await this.voting.transfer(account1, account2, 1); + + expect(await this.voting.getTotalAccountVotes(account1)).to.be.bignumber.equal('0'); + expect(await this.voting.getTotalAccountVotes(account2)).to.be.bignumber.equal('2'); + }); + }); + +}); From 054c78fdf1b42b98b27099c638c3dc0dd232ae6f Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 15:34:18 -0400 Subject: [PATCH 169/300] Add Checkpoints library tests --- test/utils/Checkpoints.test.js | 42 ++++++++++++++++++++++++++++++++++ test/utils/Voting.test.js | 7 ++++++ 2 files changed, 49 insertions(+) create mode 100644 test/utils/Checkpoints.test.js diff --git a/test/utils/Checkpoints.test.js b/test/utils/Checkpoints.test.js new file mode 100644 index 00000000000..dbca463ccdf --- /dev/null +++ b/test/utils/Checkpoints.test.js @@ -0,0 +1,42 @@ +const { expectRevert } = require('@openzeppelin/test-helpers'); + +const { expect } = require('chai'); + +const CheckpointsImpl = artifacts.require('CheckpointsImpl'); + +contract('Checkpoints', function (accounts) { + beforeEach(async function () { + this.checkpoint = await CheckpointsImpl.new(); + + this.tx1 = await this.checkpoint.push(1); + this.tx2 = await this.checkpoint.push(2); + this.tx3 = await this.checkpoint.push(3); + }); + + it('calls length', async function () { + expect(await this.checkpoint.length()).to.be.bignumber.equal('3'); + }); + + it('calls at', async function () { + expect((await this.checkpoint.at(0))[1]).to.be.bignumber.equal('1'); + expect((await this.checkpoint.at(1))[1]).to.be.bignumber.equal('2'); + expect((await this.checkpoint.at(2))[1]).to.be.bignumber.equal('3'); + }); + + it('calls latest', async function () { + expect(await this.checkpoint.latest()).to.be.bignumber.equal('3'); + }); + + it('calls past', async function () { + expect(await this.checkpoint.past(this.tx1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.checkpoint.past(this.tx2.receipt.blockNumber - 1)).to.be.bignumber.equal('1'); + expect(await this.checkpoint.past(this.tx3.receipt.blockNumber - 1)).to.be.bignumber.equal('2'); + }); + + it('reverts if block number >= current block', async function () { + await expectRevert( + this.checkpoint.past(this.tx3.receipt.blockNumber + 1), + 'block not yet mined', + ); + }); +}); diff --git a/test/utils/Voting.test.js b/test/utils/Voting.test.js index 7f883886179..8e3ecb481d7 100644 --- a/test/utils/Voting.test.js +++ b/test/utils/Voting.test.js @@ -30,6 +30,13 @@ contract('Voting', function (accounts) { expect(await this.voting.getTotalVotesAt(this.tx3.receipt.blockNumber-1)).to.be.bignumber.equal('2'); }); + it('reverts if block number >= current block', async function () { + await expectRevert( + this.voting.getTotalVotesAt(this.tx3.receipt.blockNumber + 1), + 'block not yet mined', + ); + }); + it('burns', async function () { await this.voting.burn(account1,1); expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('2'); From 92936c8ed51bef7ccc71fdde240b86f049b366f6 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 15:49:33 -0400 Subject: [PATCH 170/300] Add more documentation for voting library --- contracts/utils/Voting.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol index 0f33096c92c..260b90f45d0 100644 --- a/contracts/utils/Voting.sol +++ b/contracts/utils/Voting.sol @@ -44,7 +44,7 @@ library Voting { } /** - * @dev Get number of checkpoints for `account`. + * @dev Get number of checkpoints for `account` including delegation. */ function getTotalAccountVotes(Votes storage self, address account) internal view returns (uint256) { return self._userCheckpoints[account].length(); From c7c9b29b90f310f4437ea1a01806c66cefe97c1c Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 16:00:13 -0400 Subject: [PATCH 171/300] Lint run --- test/utils/Checkpoints.test.js | 6 +++--- test/utils/Voting.test.js | 26 ++++++++++++-------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/test/utils/Checkpoints.test.js b/test/utils/Checkpoints.test.js index dbca463ccdf..d61294260c3 100644 --- a/test/utils/Checkpoints.test.js +++ b/test/utils/Checkpoints.test.js @@ -7,7 +7,7 @@ const CheckpointsImpl = artifacts.require('CheckpointsImpl'); contract('Checkpoints', function (accounts) { beforeEach(async function () { this.checkpoint = await CheckpointsImpl.new(); - + this.tx1 = await this.checkpoint.push(1); this.tx2 = await this.checkpoint.push(2); this.tx3 = await this.checkpoint.push(3); @@ -17,7 +17,7 @@ contract('Checkpoints', function (accounts) { expect(await this.checkpoint.length()).to.be.bignumber.equal('3'); }); - it('calls at', async function () { + it('calls at', async function () { expect((await this.checkpoint.at(0))[1]).to.be.bignumber.equal('1'); expect((await this.checkpoint.at(1))[1]).to.be.bignumber.equal('2'); expect((await this.checkpoint.at(2))[1]).to.be.bignumber.equal('3'); @@ -30,7 +30,7 @@ contract('Checkpoints', function (accounts) { it('calls past', async function () { expect(await this.checkpoint.past(this.tx1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); expect(await this.checkpoint.past(this.tx2.receipt.blockNumber - 1)).to.be.bignumber.equal('1'); - expect(await this.checkpoint.past(this.tx3.receipt.blockNumber - 1)).to.be.bignumber.equal('2'); + expect(await this.checkpoint.past(this.tx3.receipt.blockNumber - 1)).to.be.bignumber.equal('2'); }); it('reverts if block number >= current block', async function () { diff --git a/test/utils/Voting.test.js b/test/utils/Voting.test.js index 8e3ecb481d7..06afa2736ba 100644 --- a/test/utils/Voting.test.js +++ b/test/utils/Voting.test.js @@ -5,7 +5,7 @@ const { expect } = require('chai'); const VotingImp = artifacts.require('VotingImpl'); contract('Voting', function (accounts) { - const [ account1, account2, account3 ] = accounts; + const [ account1, account2, account3 ] = accounts; beforeEach(async function () { this.voting = await VotingImp.new(); }); @@ -15,19 +15,18 @@ contract('Voting', function (accounts) { }); describe('move voting power', function () { - beforeEach(async function () { - this.tx1 = await this.voting.mint(account1,1); - this.tx2 = await this.voting.mint(account2,1); - this.tx3 = await this.voting.mint(account3,1); + this.tx1 = await this.voting.mint(account1, 1); + this.tx2 = await this.voting.mint(account2, 1); + this.tx3 = await this.voting.mint(account3, 1); }); it('mints', async function () { expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('3'); - - expect(await this.voting.getTotalVotesAt(this.tx1.receipt.blockNumber-1)).to.be.bignumber.equal('0'); - expect(await this.voting.getTotalVotesAt(this.tx2.receipt.blockNumber-1)).to.be.bignumber.equal('1'); - expect(await this.voting.getTotalVotesAt(this.tx3.receipt.blockNumber-1)).to.be.bignumber.equal('2'); + + expect(await this.voting.getTotalVotesAt(this.tx1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.voting.getTotalVotesAt(this.tx2.receipt.blockNumber - 1)).to.be.bignumber.equal('1'); + expect(await this.voting.getTotalVotesAt(this.tx3.receipt.blockNumber - 1)).to.be.bignumber.equal('2'); }); it('reverts if block number >= current block', async function () { @@ -38,13 +37,13 @@ contract('Voting', function (accounts) { }); it('burns', async function () { - await this.voting.burn(account1,1); + await this.voting.burn(account1, 1); expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('2'); - await this.voting.burn(account2,1); + await this.voting.burn(account2, 1); expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('1'); - await this.voting.burn(account3,1); + await this.voting.burn(account3, 1); expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('0'); }); @@ -54,7 +53,7 @@ contract('Voting', function (accounts) { expect(await this.voting.delegates(account3)).to.be.equal(account2); }); - it('transfers', async function () { + it('transfers', async function () { await this.voting.delegate(account1, account2, 1); await this.voting.transfer(account1, account2, 1); @@ -62,5 +61,4 @@ contract('Voting', function (accounts) { expect(await this.voting.getTotalAccountVotes(account2)).to.be.bignumber.equal('2'); }); }); - }); From d05cb0abfc724cb102af7518ed1881bfe044d606 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 16:33:56 -0400 Subject: [PATCH 172/300] change after rebase --- contracts/mocks/ERC721VotesMock.sol | 17 ----------------- .../ERC721/extensions/draft-ERC721Votes.sol | 10 +++++----- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 346c44914d3..b47bfd8f24e 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,16 +5,7 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/draft-ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { -<<<<<<< HEAD -<<<<<<< HEAD constructor(string memory name, string memory symbol) ERC721Votes(name, symbol) {} -======= - - constructor(string memory name, string memory symbol) ERC721Votes(name, symbol) { } ->>>>>>> Adding constructor -======= - constructor(string memory name, string memory symbol) ERC721Votes(name, symbol) {} ->>>>>>> Adding method to return token voting power function mint(address account, uint256 tokenId) public { _mint(account, tokenId); @@ -28,15 +19,7 @@ contract ERC721VotesMock is ERC721Votes { return block.chainid; } -<<<<<<< HEAD -<<<<<<< HEAD - function _maxSupply() internal pure override returns (uint224) { -======= - function _maxSupply() internal pure override returns(uint224){ ->>>>>>> Adding constructor -======= function _maxSupply() internal pure override returns (uint224) { ->>>>>>> Adding method to return token voting power return uint224(5); } } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 21da3a4975d..f09859dd613 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -172,11 +172,11 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _afterTokenTransfer( address from, - address to - ) internal virtual override{ - super._afterTokenTransfer(from, to); - - _moveVotingPower(delegates(from), delegates(to), 1); + address to, + uint256 tokenId + ) internal virtual override { + super._afterTokenTransfer(from, to, tokenId); + _votes.transfer(from, to, 1, _hookDelegateVotesChanged); } /** From b57a3dcaf5942afb99b89a184cec1c9ae186290d Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 16:43:55 -0400 Subject: [PATCH 173/300] Remove empty spaces --- test/utils/Checkpoints.test.js | 2 +- test/utils/Voting.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/utils/Checkpoints.test.js b/test/utils/Checkpoints.test.js index d52efb6bfdb..33ba473946c 100644 --- a/test/utils/Checkpoints.test.js +++ b/test/utils/Checkpoints.test.js @@ -16,7 +16,7 @@ contract('Checkpoints', function (accounts) { expect(await this.checkpoint.length()).to.be.bignumber.equal('3'); }); - it('calls at', async function () { + it('calls at', async function () { expect((await this.checkpoint.at(0))[1]).to.be.bignumber.equal('1'); expect((await this.checkpoint.at(1))[1]).to.be.bignumber.equal('2'); expect((await this.checkpoint.at(2))[1]).to.be.bignumber.equal('3'); diff --git a/test/utils/Voting.test.js b/test/utils/Voting.test.js index 3bff791753e..06afa2736ba 100644 --- a/test/utils/Voting.test.js +++ b/test/utils/Voting.test.js @@ -5,7 +5,7 @@ const { expect } = require('chai'); const VotingImp = artifacts.require('VotingImpl'); contract('Voting', function (accounts) { - const [ account1, account2, account3 ] = accounts; + const [ account1, account2, account3 ] = accounts; beforeEach(async function () { this.voting = await VotingImp.new(); }); From 02115aec688d733b5067bb09d2d842fe54ce2312 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 16:50:34 -0400 Subject: [PATCH 174/300] Run prettier --- contracts/mocks/CheckpointsImpl.sol | 2 +- contracts/mocks/VotingImpl.sol | 21 ++- .../ERC721/extensions/draft-ERC721Votes.sol | 12 +- contracts/utils/Checkpoints.sol | 35 ++--- contracts/utils/Voting.sol | 145 +++++++++++------- 5 files changed, 127 insertions(+), 88 deletions(-) diff --git a/contracts/mocks/CheckpointsImpl.sol b/contracts/mocks/CheckpointsImpl.sol index e5e0f92cd6c..ee409a45055 100644 --- a/contracts/mocks/CheckpointsImpl.sol +++ b/contracts/mocks/CheckpointsImpl.sol @@ -26,6 +26,6 @@ contract CheckpointsImpl { } function push(uint256 value) public returns (uint256, uint256) { - return _totalCheckpoints.push(value); + return _totalCheckpoints.push(value); } } diff --git a/contracts/mocks/VotingImpl.sol b/contracts/mocks/VotingImpl.sol index 6dd7990b97c..83a569ec31d 100644 --- a/contracts/mocks/VotingImpl.sol +++ b/contracts/mocks/VotingImpl.sol @@ -19,7 +19,7 @@ contract VotingImpl { function getTotalAccountVotesAt(address account, uint32 pos) public view returns (Checkpoints.Checkpoint memory) { return _votes.getTotalAccountVotesAt(account, pos); - } + } function getTotalVotes() public view returns (uint256) { return _votes.getTotalVotes(); @@ -27,7 +27,7 @@ contract VotingImpl { function getTotalAccountVotes(address account) public view returns (uint256) { return _votes.getTotalAccountVotes(account); - } + } function getTotalVotesAt(uint256 timestamp) public view returns (uint256) { return _votes.getTotalVotesAt(timestamp); @@ -37,7 +37,11 @@ contract VotingImpl { return _votes.delegates(account); } - function delegate(address account, address newDelegation, uint256 balance) public { + function delegate( + address account, + address newDelegation, + uint256 balance + ) public { return _votes.delegate(account, newDelegation, balance); } @@ -46,11 +50,14 @@ contract VotingImpl { } function burn(address from, uint256 amount) public { - return _votes.burn(from, amount); + return _votes.burn(from, amount); } - function transfer(address from, address to, uint256 amount) public { - return _votes.transfer(from, to, amount); + function transfer( + address from, + address to, + uint256 amount + ) public { + return _votes.transfer(from, to, amount); } - } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index f09859dd613..2c2b2ff7ee0 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -106,7 +106,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { require(blockNumber < block.number, "ERC721Votes: block not yet mined"); return _votes.getTotalVotesAt(blockNumber); } - + /** * @dev Delegate votes from the sender to `delegatee`. */ @@ -161,7 +161,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { function _burn(uint256 tokenId) internal virtual override { address from = ownerOf(tokenId); super._burn(tokenId); - _totalVotingPower -= 1; + _totalVotingPower -= 1; _votes.burn(from, 1, _hookDelegateVotesChanged); } @@ -176,7 +176,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { uint256 tokenId ) internal virtual override { super._afterTokenTransfer(from, to, tokenId); - _votes.transfer(from, to, 1, _hookDelegateVotesChanged); + _votes.transfer(from, to, 1, _hookDelegateVotesChanged); } /** @@ -215,7 +215,11 @@ abstract contract ERC721Votes is ERC721, EIP712 { return _domainSeparatorV4(); } - function _hookDelegateVotesChanged(address account, uint256 previousBalance, uint256 newBalance) private { + function _hookDelegateVotesChanged( + address account, + uint256 previousBalance, + uint256 newBalance + ) private { emit DelegateVotesChanged(account, previousBalance, newBalance); } } diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index 3e34f1f6933..dacd0649e4f 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -18,30 +18,30 @@ library Checkpoints { } /** - * @dev Returns checkpoints length. - */ + * @dev Returns checkpoints length. + */ function length(History storage self) internal view returns (uint256) { return self._checkpoints.length; } /** - * @dev Returns checkpoints at given position. - */ + * @dev Returns checkpoints at given position. + */ function at(History storage self, uint256 pos) internal view returns (Checkpoint memory) { return self._checkpoints[pos]; } /** - * @dev Returns total amount of checkpoints. - */ + * @dev Returns total amount of checkpoints. + */ function latest(History storage self) internal view returns (uint256) { uint256 pos = length(self); return pos == 0 ? 0 : at(self, pos - 1).value; } /** - * @dev Returns checkpoints at given block number. - */ + * @dev Returns checkpoints at given block number. + */ function past(History storage self, uint256 index) internal view returns (uint256) { require(index < block.number, "block not yet mined"); @@ -59,25 +59,24 @@ library Checkpoints { } /** - * @dev Creates checkpoint - */ - function push( - History storage self, - uint256 value - ) internal returns (uint256, uint256) { + * @dev Creates checkpoint + */ + function push(History storage self, uint256 value) internal returns (uint256, uint256) { uint256 pos = length(self); - uint256 old = latest(self); + uint256 old = latest(self); if (pos > 0 && self._checkpoints[pos - 1].index == block.number) { self._checkpoints[pos - 1].value = SafeCast.toUint224(value); } else { - self._checkpoints.push(Checkpoint({ index: SafeCast.toUint32(block.number), value: SafeCast.toUint224(value) })); + self._checkpoints.push( + Checkpoint({index: SafeCast.toUint32(block.number), value: SafeCast.toUint224(value)}) + ); } return (old, value); } /** - * @dev Creates checkpoint - */ + * @dev Creates checkpoint + */ function push( History storage self, function(uint256, uint256) view returns (uint256) op, diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol index 260b90f45d0..24d70be1635 100644 --- a/contracts/utils/Voting.sol +++ b/contracts/utils/Voting.sol @@ -10,72 +10,85 @@ library Voting { using Checkpoints for Checkpoints.History; struct Votes { - mapping(address => address) _delegation; + mapping(address => address) _delegation; mapping(address => Checkpoints.History) _userCheckpoints; - Checkpoints.History _totalCheckpoints; + Checkpoints.History _totalCheckpoints; } /** - * @dev Returns total amount of votes for account. - */ + * @dev Returns total amount of votes for account. + */ function getVotes(Votes storage self, address account) internal view returns (uint256) { return self._userCheckpoints[account].latest(); } /** - * @dev Returns total amount of votes at given position. - */ - function getVotesAt(Votes storage self, address account, uint256 timestamp) internal view returns (uint256) { + * @dev Returns total amount of votes at given position. + */ + function getVotesAt( + Votes storage self, + address account, + uint256 timestamp + ) internal view returns (uint256) { return self._userCheckpoints[account].past(timestamp); } /** - * @dev Get checkpoint for `account` for specific position. - */ - function getTotalAccountVotesAt(Votes storage self, address account, uint32 pos) internal view returns (Checkpoints.Checkpoint memory) { + * @dev Get checkpoint for `account` for specific position. + */ + function getTotalAccountVotesAt( + Votes storage self, + address account, + uint32 pos + ) internal view returns (Checkpoints.Checkpoint memory) { return self._userCheckpoints[account].at(pos); - } + } /** - * @dev Returns total amount of votes. - */ + * @dev Returns total amount of votes. + */ function getTotalVotes(Votes storage self) internal view returns (uint256) { return self._totalCheckpoints.latest(); } - + /** - * @dev Get number of checkpoints for `account` including delegation. - */ + * @dev Get number of checkpoints for `account` including delegation. + */ function getTotalAccountVotes(Votes storage self, address account) internal view returns (uint256) { return self._userCheckpoints[account].length(); - } + } /** - * @dev Returns all votes for timestamp. - */ + * @dev Returns all votes for timestamp. + */ function getTotalVotesAt(Votes storage self, uint256 timestamp) internal view returns (uint256) { return self._totalCheckpoints.past(timestamp); } /** - * @dev Returns account delegation. - */ + * @dev Returns account delegation. + */ function delegates(Votes storage self, address account) internal view returns (address) { return self._delegation[account]; } /** - * @dev Delegates voting power. - */ - function delegate(Votes storage self, address account, address newDelegation, uint256 balance) internal { + * @dev Delegates voting power. + */ + function delegate( + Votes storage self, + address account, + address newDelegation, + uint256 balance + ) internal { address oldDelegation = delegates(self, account); self._delegation[account] = newDelegation; - _moveVotingPower(self, oldDelegation, newDelegation, balance, _dummy); + _moveVotingPower(self, oldDelegation, newDelegation, balance, _dummy); } /** - * @dev Delegates voting power. - */ + * @dev Delegates voting power. + */ function delegate( Votes storage self, address account, @@ -85,20 +98,24 @@ library Voting { ) internal { address oldDelegation = delegates(self, account); self._delegation[account] = newDelegation; - _moveVotingPower(self, oldDelegation, newDelegation, balance, hookDelegateVotesChanged); + _moveVotingPower(self, oldDelegation, newDelegation, balance, hookDelegateVotesChanged); } /** - * @dev Mints new vote. - */ - function mint(Votes storage self, address to, uint256 amount) internal { + * @dev Mints new vote. + */ + function mint( + Votes storage self, + address to, + uint256 amount + ) internal { self._totalCheckpoints.push(_add, amount); - _moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); + _moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); } /** - * @dev Mints new vote. - */ + * @dev Mints new vote. + */ function mint( Votes storage self, address to, @@ -106,20 +123,24 @@ library Voting { function(address, uint256, uint256) hookDelegateVotesChanged ) internal { self._totalCheckpoints.push(_add, amount); - _moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); + _moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); } /** - * @dev Burns new vote. - */ - function burn(Votes storage self, address from, uint256 amount) internal { + * @dev Burns new vote. + */ + function burn( + Votes storage self, + address from, + uint256 amount + ) internal { self._totalCheckpoints.push(_subtract, amount); - _moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); + _moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); } /** - * @dev Burns new vote. - */ + * @dev Burns new vote. + */ function burn( Votes storage self, address from, @@ -127,19 +148,24 @@ library Voting { function(address, uint256, uint256) hookDelegateVotesChanged ) internal { self._totalCheckpoints.push(_subtract, amount); - _moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); + _moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); } /** - * @dev Transfers voting power. - */ - function transfer(Votes storage self, address from, address to, uint256 amount) internal { - _moveVotingPower(self, delegates(self, from), delegates(self, to), amount, _dummy); + * @dev Transfers voting power. + */ + function transfer( + Votes storage self, + address from, + address to, + uint256 amount + ) internal { + _moveVotingPower(self, delegates(self, from), delegates(self, to), amount, _dummy); } /** - * @dev Transfers voting power. - */ + * @dev Transfers voting power. + */ function transfer( Votes storage self, address from, @@ -147,13 +173,13 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) internal { - _moveVotingPower(self, delegates(self, from), delegates(self, to), amount, hookDelegateVotesChanged); + _moveVotingPower(self, delegates(self, from), delegates(self, to), amount, hookDelegateVotesChanged); } /** - * @dev Moves voting power. - */ - function _moveVotingPower( + * @dev Moves voting power. + */ + function _moveVotingPower( Votes storage self, address src, address dst, @@ -173,19 +199,22 @@ library Voting { } /** - * @dev Adds two numbers. - */ + * @dev Adds two numbers. + */ function _add(uint256 a, uint256 b) private pure returns (uint256) { return a + b; } /** - * @dev Subtracts two numbers. - */ + * @dev Subtracts two numbers. + */ function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } - function _dummy(address, uint256, uint256) private pure {} - + function _dummy( + address, + uint256, + uint256 + ) private pure {} } From 51002f918c65448063c16b436d3c1e7db7b74cb0 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 29 Nov 2021 11:35:43 -0400 Subject: [PATCH 175/300] Remove whitespaces --- test/token/ERC721/extensions/ERC721Votes.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 0ef80342a7d..ed44858e6f6 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -382,7 +382,7 @@ contract('ERC721Votes', function (accounts) { describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { - await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); From 54d80dbd2a29c78ba02d445025de9770a6e14108 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 29 Nov 2021 11:37:13 -0400 Subject: [PATCH 176/300] Remove whitespaces --- test/token/ERC721/extensions/ERC721Votes.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index ed44858e6f6..33af029f6ee 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -385,7 +385,7 @@ contract('ERC721Votes', function (accounts) { await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - + const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); From acd9a0009f7576d73f933ecb93a50afa6157815c Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 29 Nov 2021 13:33:30 -0400 Subject: [PATCH 177/300] Update changelog after merge --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b0e9494ef..9c13adfe6b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,13 @@ ## Unreleased +* `Voting`: Create library to be use for ERC721 and ERC1155 voting ([#2944](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2944)) * `GovernorTimelockControl`: improve the `state()` function to have it reflect cases where a proposal has been canceled directly on the timelock. ([#2977](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2977)) * `Math`: add a `abs(int256)` method that returns the unsigned absolute value of a signed value. ([#2984](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2984)) * Preset contracts are now deprecated in favor of [Contracts Wizard](https://wizard.openzeppelin.com). ([#2986](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2986)) ## 4.4.0 (2021-11-25) -## Unreleased - * `Voting`: Create library to be use for ERC721 and ERC1155 voting ([#2944](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2944)) * `Ownable`: add an internal `_transferOwnership(address)`. ([#2568](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2568)) * `AccessControl`: add internal `_grantRole(bytes32,address)` and `_revokeRole(bytes32,address)`. ([#2568](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2568)) * `AccessControl`: mark `_setupRole(bytes32,address)` as deprecated in favor of `_grantRole(bytes32,address)`. ([#2568](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2568)) From c91998028640e601206a772f0da84dbafa7ac8ee Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sat, 30 Oct 2021 20:49:29 -0400 Subject: [PATCH 178/300] Initial contracts creation --- .../extensions/GovernorVotesERC721.sol | 28 ++ .../token/ERC721/extensions/ERC721Votes.sol | 258 ++++++++++++++++++ .../ERC721/extensions/draft-ERC721Permit.sol | 87 ++++++ .../ERC721/extensions/draft-IERC721Permit.sol | 60 ++++ 4 files changed, 433 insertions(+) create mode 100644 contracts/governance/extensions/GovernorVotesERC721.sol create mode 100644 contracts/token/ERC721/extensions/ERC721Votes.sol create mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol create mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol new file mode 100644 index 00000000000..9a2a4ac0ee8 --- /dev/null +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC72GovernorVotesERC721.sol) + +pragma solidity ^0.8.0; + +import "../Governor.sol"; +import "../../token/ERC721/extensions/ERC721Votes.sol"; +import "../../utils/math/Math.sol"; + +/** + * @dev Extension of {Governor} for voting weight extraction from an {ERC721Votes} token. + * + * _Available since v4.3._ + */ +abstract contract ERC72GovernorVotesERC721 is Governor { + ERC721Votes public immutable token; + + constructor(ERC721Votes tokenAddress) { + token = tokenAddress; + } + + /** + * Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). + */ + function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { + return token.getPastVotes(account, blockNumber); + } +} diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol new file mode 100644 index 00000000000..20da35f32a6 --- /dev/null +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/ERC721Votes.sol) + +pragma solidity ^0.8.0; + +import "./draft-ERC721Permit.sol"; +import "../../../utils/math/Math.sol"; +import "../../../utils/math/SafeCast.sol"; +import "../../../utils/cryptography/ECDSA.sol"; + +/** + * @dev Extension of ERC721 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 {ERC721VotesComp} 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 {getVotes} and {getPastVotes}. + * + * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it + * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. + * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this + * will significantly increase the base gas cost of transfers. + * + * _Available since v4.2._ + */ +abstract contract ERC721Votes is ERC721Permit { + struct Checkpoint { + uint32 fromBlock; + uint224 votes; + } + + bytes32 private constant _DELEGATION_TYPEHASH = + keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + + mapping(address => address) private _delegates; + mapping(address => Checkpoint[]) private _checkpoints; + Checkpoint[] private _totalSupplyCheckpoints; + + /** + * @dev Emitted when an account changes their delegate. + */ + event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); + + /** + * @dev Emitted when a token transfer or delegate change results in changes to an account's voting power. + */ + event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); + + /** + * @dev Get the `pos`-th checkpoint for `account`. + */ + function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoint memory) { + return _checkpoints[account][pos]; + } + + /** + * @dev Get number of checkpoints for `account`. + */ + function numCheckpoints(address account) public view virtual returns (uint32) { + return SafeCast.toUint32(_checkpoints[account].length); + } + + /** + * @dev Get the address `account` is currently delegating to. + */ + function delegates(address account) public view virtual returns (address) { + return _delegates[account]; + } + + /** + * @dev Gets the current votes balance for `account` + */ + function getVotes(address account) public view returns (uint256) { + uint256 pos = _checkpoints[account].length; + return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes; + } + + /** + * @dev Retrieve the number of votes for `account` at the end of `blockNumber`. + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) { + require(blockNumber < block.number, "ERC721Votes: block not yet mined"); + return _checkpointsLookup(_checkpoints[account], blockNumber); + } + + /** + * @dev Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances. + * It is but NOT the sum of all the delegated votes! + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastTotalSupply(uint256 blockNumber) public view returns (uint256) { + require(blockNumber < block.number, "ERC721Votes: block not yet mined"); + return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber); + } + + /** + * @dev Lookup a value in a list of (sorted) checkpoints. + */ + function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) private view returns (uint256) { + // We run a binary search to look for the earliest checkpoint taken after `blockNumber`. + // + // During the loop, the index of the wanted checkpoint remains in the range [low-1, high). + // With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant. + // - If the middle checkpoint is after `blockNumber`, we look in [low, mid) + // - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high) + // Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not + // out of bounds (in which case we're looking too far in the past and the result is 0). + // Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is + // past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out + // the same. + uint256 high = ckpts.length; + uint256 low = 0; + while (low < high) { + uint256 mid = Math.average(low, high); + if (ckpts[mid].fromBlock > blockNumber) { + high = mid; + } else { + low = mid + 1; + } + } + + return high == 0 ? 0 : ckpts[high - 1].votes; + } + + /** + * @dev Delegate votes from the sender to `delegatee`. + */ + function delegate(address delegatee) public virtual { + _delegate(_msgSender(), delegatee); + } + + /** + * @dev Delegates votes from signer to `delegatee` + */ + function delegateBySig( + address delegatee, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + require(block.timestamp <= expiry, "ERC721Votes: signature expired"); + address signer = ECDSA.recover( + _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), + v, + r, + s + ); + require(nonce == _useNonce(signer), "ERC721Votes: invalid nonce"); + _delegate(signer, delegatee); + } + + /** + * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). + */ + function _maxSupply() internal view virtual returns (uint224) { + return type(uint224).max; + } + + /** + * @dev Snapshots the totalSupply after it has been increased. + */ + function _mint(address account, uint256 amount) internal virtual override { + super._mint(account, amount);//TODO: update for NFT + require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + + _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); + } + + /** + * @dev Snapshots the totalSupply after it has been decreased. + */ + function _burn(uint256 tokenId) internal virtual override { + super._burn(tokenId); + + _writeCheckpoint(_totalSupplyCheckpoints, _subtract, tokenId); + } + + /** + * @dev Move voting power when tokens are transferred. + * + * Emits a {DelegateVotesChanged} event. + */ + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual { + _moveVotingPower(delegates(from), delegates(to), amount);//TODO: Update to be NFT logic + } + + /** + * @dev Change delegation for `delegator` to `delegatee`. + * + * Emits events {DelegateChanged} and {DelegateVotesChanged}. + */ + function _delegate(address delegator, address delegatee) internal virtual { + address currentDelegate = delegates(delegator); + uint256 delegatorBalance = balanceOf(delegator); + _delegates[delegator] = delegatee; + + emit DelegateChanged(delegator, currentDelegate, delegatee); + + _moveVotingPower(currentDelegate, delegatee, delegatorBalance); + } + + function _moveVotingPower( + address src, + address dst, + uint256 amount + ) private { + if (src != dst && amount > 0) { + if (src != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[src], _subtract, amount); + emit DelegateVotesChanged(src, oldWeight, newWeight); + } + + if (dst != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[dst], _add, amount); + emit DelegateVotesChanged(dst, oldWeight, newWeight); + } + } + } + + function _writeCheckpoint(//TODO: update for NFT + Checkpoint[] storage ckpts, + function(uint256, uint256) view returns (uint256) op, + uint256 delta + ) private returns (uint256 oldWeight, uint256 newWeight) { + uint256 pos = ckpts.length; + oldWeight = pos == 0 ? 0 : ckpts[pos - 1].votes; + newWeight = op(oldWeight, delta); + + if (pos > 0 && ckpts[pos - 1].fromBlock == block.number) { + ckpts[pos - 1].votes = SafeCast.toUint224(newWeight); + } else { + ckpts.push(Checkpoint({fromBlock: SafeCast.toUint32(block.number), votes: SafeCast.toUint224(newWeight)})); + } + } + + function _add(uint256 a, uint256 b) private pure returns (uint256) { + return a + b; + } + + function _subtract(uint256 a, uint256 b) private pure returns (uint256) { + return a - b; + } +} diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol new file mode 100644 index 00000000000..c00d15367ab --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) + +pragma solidity ^0.8.0; + +import "./draft-IERC721Permit.sol"; +import "./ERC721Enumerable.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; +import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/Counters.sol"; + +/** + * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * _Available since v3.4._ + */ +abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { + using Counters for Counters.Counter; + + mapping(address => Counters.Counter) private _nonces; + + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC721 token name. + */ + constructor(string memory name) EIP712(name, "1") {} + + /** + * @dev See {IERC721Permit-permit}. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); + + bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + + bytes32 hash = _hashTypedDataV4(structHash); + + address signer = ECDSA.recover(hash, v, r, s); + require(signer == owner, "ERC721Permit: invalid signature"); + + _approve(spender, value); + } + + /** + * @dev See {IERC721Permit-nonces}. + */ + function nonces(address owner) public view virtual override returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } +} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol new file mode 100644 index 00000000000..61882f2de0d --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + */ +interface IERC721Permit { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC721-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} From 5a101535e173a0712825261eca628a09bde14665 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 09:28:35 -0400 Subject: [PATCH 179/300] creating permit tests --- contracts/mocks/ERC721PermitMock.sol | 20 + contracts/mocks/ERC721VotesMock.sol | 21 + .../ERC721/extensions/ERC721Votes.test.js | 538 ++++++++++++++++++ .../extensions/draft-ERC721Permit.test.js | 117 ++++ 4 files changed, 696 insertions(+) create mode 100644 contracts/mocks/ERC721PermitMock.sol create mode 100644 contracts/mocks/ERC721VotesMock.sol create mode 100644 test/token/ERC721/extensions/ERC721Votes.test.js create mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol new file mode 100644 index 00000000000..37a860ef4dc --- /dev/null +++ b/contracts/mocks/ERC721PermitMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC721/extensions/draft-ERC721Permit.sol"; + +contract ERC721PermitMock is ERC721Permit { + constructor( + string memory name, + string memory symbol, + address initialAccount, + uint256 tokenId + ) payable ERC721(name, symbol) ERC721Permit(name) { + _mint(initialAccount, tokenId); + } + + function getChainId() external view returns (uint256) { + return block.chainid; + } +} diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol new file mode 100644 index 00000000000..457d05eedb9 --- /dev/null +++ b/contracts/mocks/ERC721VotesMock.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC721/extensions/ERC721Votes.sol"; + +contract ERC721VotesMock is ERC721Votes { + constructor(string memory name, string memory symbol) ERC721(name, symbol) ERC721Permit(name) {} + + function mint(address account, uint256 tokenId) public { + _mint(account, tokenId); + } + + function burn(uint256 tokenId) public { + _burn(tokenId); + } + + function getChainId() external view returns (uint256) { + return block.chainid; + } +} diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js new file mode 100644 index 00000000000..7078828039d --- /dev/null +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -0,0 +1,538 @@ +/* eslint-disable */ + +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const { promisify } = require('util'); +const queue = promisify(setImmediate); + +const ERC721VotesMock = artifacts.require('ERC721VotesMock'); + +const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); + +const Delegation = [ + { name: 'delegatee', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'expiry', type: 'uint256' }, +]; + +async function countPendingTransactions() { + return parseInt( + await network.provider.send('eth_getBlockTransactionCountByNumber', ['pending']) + ); +} + +async function batchInBlock (txs) { + try { + // disable auto-mining + await network.provider.send('evm_setAutomine', [false]); + // send all transactions + const promises = txs.map(fn => fn()); + // wait for node to have all pending transactions + while (txs.length > await countPendingTransactions()) { + await queue(); + } + // mine one block + await network.provider.send('evm_mine'); + // fetch receipts + const receipts = await Promise.all(promises); + // Sanity check, all tx should be in the same block + const minedBlocks = new Set(receipts.map(({ receipt }) => receipt.blockNumber)); + expect(minedBlocks.size).to.equal(1); + + return receipts; + } finally { + // enable auto-mining + await network.provider.send('evm_setAutomine', [true]); + } +} + +contract('ERC721Votes', function (accounts) { + const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; + const supply = new BN('10000000000000000000000000'); + + beforeEach(async function () { + this.token = await ERC721VotesMock.new(name, symbol); + + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = await this.token.getChainId(); + }); + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect( + await this.token.DOMAIN_SEPARATOR(), + ).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address), + ); + }); + + it('minting restriction', async function () { + const amount = new BN('2').pow(new BN('224')); + await expectRevert( + this.token.mint(holder, amount), + 'ERC721Votes: total supply risks overflowing votes', + ); + }); + + describe('set delegation', function () { + describe('call', function () { + it('delegation with balance', async function () { + await this.token.mint(holder, supply); + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + + expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); + }); + + it('delegation without balance', async function () { + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + }); + }); + + describe('with signature', function () { + const delegator = Wallet.generate(); + const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); + const nonce = 0; + + const buildData = (chainId, verifyingContract, message) => ({ data: { + primaryType: 'Delegation', + types: { EIP712Domain, Delegation }, + domain: { name, version, chainId, verifyingContract }, + message, + }}); + + beforeEach(async function () { + await this.token.mint(delegatorAddress, supply); + }); + + it('accept signed delegation', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + expectEvent(receipt, 'DelegateChanged', { + delegator: delegatorAddress, + fromDelegate: ZERO_ADDRESS, + toDelegate: delegatorAddress, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: delegatorAddress, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); + + expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); + }); + + it('rejects reused signature', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), + 'ERC721Votes: invalid nonce', + ); + }); + + it('rejects bad delegatee', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + const { logs } = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); + const { args } = logs.find(({ event }) => event == 'DelegateChanged'); + expect(args.delegator).to.not.be.equal(delegatorAddress); + expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); + expect(args.toDelegate).to.be.equal(holderDelegatee); + }); + + it('rejects bad nonce', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), + 'ERC721Votes: invalid nonce', + ); + }); + + it('rejects expired permit', async function () { + const expiry = (await time.latest()) - time.duration.weeks(1); + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry, + }), + )); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), + 'ERC721Votes: signature expired', + ); + }); + }); + }); + + describe('change delegation', function () { + beforeEach(async function () { + await this.token.mint(holder, supply); + await this.token.delegate(holder, { from: holder }); + }); + + it('call', async function () { + expect(await this.token.delegates(holder)).to.be.equal(holder); + + const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: holder, + toDelegate: holderDelegatee, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: supply, + newBalance: '0', + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holderDelegatee, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); + + expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); + expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); + }); + }); + + 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.getVotes(holder)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); + + // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" + const blockNumber = await time.latestBlock(); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getPastVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes); + }); + }); + + // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js. + describe('Compound test suite', function () { + beforeEach(async function () { + await this.token.mint(holder, supply); + }); + + describe('balanceOf', function () { + it('grants to initial account', async function () { + expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); + }); + }); + + describe('numCheckpoints', function () { + it('returns the number of checkpoints for a delegate', async function () { + await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const t1 = await this.token.delegate(other1, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + + const t2 = await this.token.transfer(other2, 10, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + + const t3 = await this.token.transfer(other2, 10, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); + + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '100' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '90' ]); + expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '80' ]); + expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + + await time.advanceBlock(); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); + }); + + it('does not add more than one checkpoint in a block', async function () { + await this.token.transfer(recipient, '100', { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const [ t1, t2, t3 ] = await batchInBlock([ + () => this.token.delegate(other1, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + ]); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); + // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check + // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + }); + }); + + describe('getPastVotes', function () { + it('reverts if block number >= current block', async function () { + await expectRevert( + this.token.getPastVotes(other1, 5e10), + 'ERC721Votes: block not yet mined', + ); + }); + + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.transfer(holder, 20, { from: other2 }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + }); + }); + + describe('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), + 'ERC721Votes: block not yet mined', + ); + }); + + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + t1 = await this.token.mint(holder, supply); + + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const t1 = await this.token.mint(holder, supply); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.mint(holder, supply); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.burn(10); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.burn(10); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.mint(holder, 20); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + }); +}); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js new file mode 100644 index 00000000000..7a7b8cf5d83 --- /dev/null +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -0,0 +1,117 @@ +/* eslint-disable */ + +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const ERC721PermitMock = artifacts.require('ERC721PermitMock'); + +const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); + +const Permit = [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, +]; + +contract('ERC721Permit', function (accounts) { + const [ initialHolder, spender, recipient, other ] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; + + const initialTokenId = new BN('100'); + + beforeEach(async function () { + this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); + + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = await this.token.getChainId(); + }); + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect( + await this.token.DOMAIN_SEPARATOR(), + ).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address), + ); + }); + + describe.only('permit', function () { + const wallet = Wallet.generate(); + + const owner = wallet.getAddressString(); + const value = initialTokenId; + const nonce = 0; + const maxDeadline = MAX_UINT256; + + const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ + primaryType: 'Permit', + types: { EIP712Domain, Permit }, + domain: { name, version, chainId, verifyingContract }, + message: { owner, spender, value, nonce, deadline }, + }); + + it('accepts owner signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); + expect(await this.token.getApproved(value)).to.be.equal(spender); + }); + + it('rejects reused signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects other signature', async function () { + const otherWallet = Wallet.generate(); + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects expired permit', async function () { + const deadline = (await time.latest()) - time.duration.weeks(1); + + const data = buildData(this.chainId, this.token.address, deadline); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, deadline, v, r, s), + 'ERC721Permit: expired deadline', + ); + }); + }); +}); From 792ce186adce08aa9a86760a93283939bf3a7e08 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 10:26:22 -0400 Subject: [PATCH 180/300] Fixing checkpoints count --- .../token/ERC721/extensions/ERC721Votes.sol | 11 ++--- .../ERC721/extensions/ERC721Votes.test.js | 47 ++++++++++++------- .../extensions/draft-ERC721Permit.test.js | 2 +- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 20da35f32a6..b184c9c83fa 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -7,7 +7,6 @@ import "./draft-ERC721Permit.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; - /** * @dev Extension of ERC721 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. @@ -170,11 +169,11 @@ abstract contract ERC721Votes is ERC721Permit { /** * @dev Snapshots the totalSupply after it has been increased. */ - function _mint(address account, uint256 amount) internal virtual override { - super._mint(account, amount);//TODO: update for NFT + function _mint(address account, uint256 tokenId) internal virtual override { + super._mint(account, tokenId); require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } /** @@ -183,7 +182,7 @@ abstract contract ERC721Votes is ERC721Permit { function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); - _writeCheckpoint(_totalSupplyCheckpoints, _subtract, tokenId); + _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); } /** @@ -232,7 +231,7 @@ abstract contract ERC721Votes is ERC721Permit { } } - function _writeCheckpoint(//TODO: update for NFT + function _writeCheckpoint( Checkpoint[] storage ckpts, function(uint256, uint256) view returns (uint256) op, uint256 delta diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 7078828039d..d2981dd5f9d 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -54,7 +54,9 @@ async function batchInBlock (txs) { contract('ERC721Votes', function (accounts) { const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; - + const NFT1 = new BN('10'); + const NFT2 = new BN('20'); + const NFT3 = new BN('30'); const name = 'My Token'; const symbol = 'MTKN'; const version = '1'; @@ -91,7 +93,7 @@ contract('ERC721Votes', function (accounts) { describe('set delegation', function () { describe('call', function () { - it('delegation with balance', async function () { + it('delegation with balance', async function () {//TODO: Make it NFT like await this.token.mint(holder, supply); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); @@ -249,7 +251,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, supply);//TODO: Avoid tokenId duplicate await this.token.delegate(holder, { from: holder }); }); @@ -359,6 +361,9 @@ contract('ERC721Votes', function (accounts) { describe('Compound test suite', function () { beforeEach(async function () { await this.token.mint(holder, supply); + await this.token.mint(holder, NFT1); + await this.token.mint(holder, NFT2); + await this.token.mint(holder, NFT3); }); describe('balanceOf', function () { @@ -448,16 +453,16 @@ contract('ERC721Votes', function (accounts) { }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); + const t1 = await this.token.delegate(other1, { from: holder });//TODO: Make it NFT like 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 }); + const t3 = await this.token.transfer(other2, 20, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 20, { from: other2 }); + const t4 = await this.token.transfer(holder, 30, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); @@ -511,28 +516,34 @@ contract('ERC721Votes', function (accounts) { }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.mint(holder, supply); + const t1 = await this.token.mint(holder, NFT1); await time.advanceBlock(); await time.advanceBlock(); - const t2 = await this.token.burn(10); + const t2 = await this.token.burn(NFT1); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.burn(10); + const t3 = await this.token.mint(holder, NFT2); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.mint(holder, 20); + const t4 = await this.token.burn(NFT2); + await time.advanceBlock(); + await time.advanceBlock(); + const t5 = await this.token.mint(holder, NFT3); await time.advanceBlock(); await time.advanceBlock(); + console.log(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); }); }); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js index 7a7b8cf5d83..f22c904c724 100644 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -50,7 +50,7 @@ contract('ERC721Permit', function (accounts) { ); }); - describe.only('permit', function () { + describe('permit', function () { const wallet = Wallet.generate(); const owner = wallet.getAddressString(); From 71301aae84faaae060dfe38a98741f3f494c07ad Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:20:14 -0400 Subject: [PATCH 181/300] Updating ERC721Vote tests --- contracts/mocks/ERC721VotesMock.sol | 4 + .../token/ERC721/extensions/ERC721Votes.sol | 18 ++- .../ERC721/extensions/ERC721Votes.test.js | 146 +++++++++--------- 3 files changed, 96 insertions(+), 72 deletions(-) diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 457d05eedb9..09e40a4ea85 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -18,4 +18,8 @@ contract ERC721VotesMock is ERC721Votes { function getChainId() external view returns (uint256) { return block.chainid; } + + function _maxSupply() internal pure override returns(uint224){ + return uint224(4); + } } diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index b184c9c83fa..146ff1dd213 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -192,10 +192,9 @@ abstract contract ERC721Votes is ERC721Permit { */ function _afterTokenTransfer( address from, - address to, - uint256 amount + address to ) internal virtual { - _moveVotingPower(delegates(from), delegates(to), amount);//TODO: Update to be NFT logic + _moveVotingPower(delegates(from), delegates(to), 1); } /** @@ -254,4 +253,17 @@ abstract contract ERC721Votes is ERC721Permit { function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } + + /** + * @dev Moves token from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 tokenId) external returns (bool){ + _transfer(_msgSender(), recipient, tokenId); + _afterTokenTransfer(_msgSender(), recipient); + return true; + } } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index d2981dd5f9d..b2d56ea7ab0 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -14,6 +14,7 @@ const queue = promisify(setImmediate); const ERC721VotesMock = artifacts.require('ERC721VotesMock'); const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); +const { Console } = require('console'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -84,9 +85,14 @@ contract('ERC721Votes', function (accounts) { }); it('minting restriction', async function () { - const amount = new BN('2').pow(new BN('224')); + const lastTokenId = new BN('2').pow(new BN('224')); + this.token.mint(holder, NFT1); + this.token.mint(holder, NFT2); + this.token.mint(holder, NFT3); + this.token.mint(holder, supply); + await expectRevert( - this.token.mint(holder, amount), + this.token.mint(holder, lastTokenId), 'ERC721Votes: total supply risks overflowing votes', ); }); @@ -106,15 +112,15 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '0', - newBalance: supply, + newBalance: '1', }); expect(await this.token.delegates(holder)).to.be.equal(holder); - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); + expect(await this.token.getVotes(holder)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); }); it('delegation without balance', async function () { @@ -169,15 +175,15 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: delegatorAddress, previousBalance: '0', - newBalance: supply, + newBalance: '1', }); expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); - expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); + expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1'); }); it('rejects reused signature', async function () { @@ -251,7 +257,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { - await this.token.mint(holder, supply);//TODO: Avoid tokenId duplicate + await this.token.mint(holder, supply); await this.token.delegate(holder, { from: holder }); }); @@ -266,24 +272,24 @@ contract('ERC721Votes', function (accounts) { }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, - previousBalance: supply, + previousBalance: '1', newBalance: '0', }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holderDelegatee, previousBalance: '0', - newBalance: supply, + newBalance: '1', }); expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); - expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); + expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); }); }); @@ -293,8 +299,8 @@ contract('ERC721Votes', function (accounts) { }); it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -304,22 +310,22 @@ contract('ERC721Votes', function (accounts) { 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 { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); 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.holderVotes = '0'; 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' }); + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -333,15 +339,15 @@ contract('ERC721Votes', function (accounts) { 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) }); + const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); 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.holderVotes = '0'; this.recipientVotes = '1'; }); @@ -368,54 +374,55 @@ contract('ERC721Votes', function (accounts) { describe('balanceOf', function () { it('grants to initial account', async function () { - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('4'); }); }); 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 + + await this.token.transfer(recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transfer(recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - - const t2 = await this.token.transfer(other2, 10, { from: recipient }); + + const t2 = await this.token.transfer(other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - - const t3 = await this.token.transfer(other2, 10, { from: recipient }); + + const t3 = await this.token.transfer(other2, NFT2, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - const t4 = await this.token.transfer(recipient, 20, { from: holder }); + const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '100' ]); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '90' ]); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '80' ]); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); + expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); + expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('1'); }); it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, '100', { from: holder }); + await this.token.transfer(recipient, NFT1, { from: holder }); + await this.token.transfer(recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const [ t1, t2, t3 ] = await batchInBlock([ () => this.token.delegate(other1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, NFT1, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, NFT2, { from: recipient, gas: 100000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); - // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - const t4 = await this.token.transfer(recipient, 20, { from: holder }); + const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); }); @@ -438,8 +445,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('4'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); }); it('returns zero if < first checkpoint block', async function () { @@ -449,39 +456,41 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder });//TODO: Make it NFT like - await time.advanceBlock(); + const total = await this.token.balanceOf(holder); + + const t1 = await this.token.delegate(other1, { from: holder }); await time.advanceBlock(); - const t2 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + const t2 = await this.token.transfer(other2, NFT1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.transfer(other2, 20, { from: holder }); + const t3 = await this.token.transfer(other2, NFT2, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 30, { from: other2 }); + const t4 = await this.token.transfer(holder, NFT2, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); - + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal(total); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(total); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); }); }); }); describe('getPastTotalSupply', function () { beforeEach(async function () { - await this.token.delegate(holder, { from: holder }); + // await this.token.delegate(holder, { from: holder }); }); it('reverts if block number >= current block', async function () { @@ -501,8 +510,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('returns zero if < first checkpoint block', async function () { @@ -512,7 +521,7 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { @@ -532,7 +541,6 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - console.log(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); From 9c39e321742655fba158621f86793a603471b1e8 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:23:31 -0400 Subject: [PATCH 182/300] Updating ERC721Vote tests descriptions --- test/token/ERC721/extensions/ERC721Votes.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index b2d56ea7ab0..49592f36fc8 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -99,7 +99,7 @@ contract('ERC721Votes', function (accounts) { describe('set delegation', function () { describe('call', function () { - it('delegation with balance', async function () {//TODO: Make it NFT like + it('delegation with tokenId', async function () { await this.token.mint(holder, supply); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); @@ -123,7 +123,7 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); }); - it('delegation without balance', async function () { + it('delegation without tokenId', async function () { expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); From a3396c5a192b86f02f265eddc977d3f208b81e9c Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:55:45 -0400 Subject: [PATCH 183/300] Updating ERC721Vote contract and tests --- contracts/token/ERC721/extensions/ERC721Votes.sol | 2 -- test/token/ERC721/extensions/ERC721Votes.test.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 146ff1dd213..978bcd0787e 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -11,8 +11,6 @@ import "../../../utils/cryptography/ECDSA.sol"; * @dev Extension of ERC721 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 {ERC721VotesComp} 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 {getVotes} and {getPastVotes}. diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 49592f36fc8..9712e69d21e 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -490,7 +490,7 @@ contract('ERC721Votes', function (accounts) { describe('getPastTotalSupply', function () { beforeEach(async function () { - // await this.token.delegate(holder, { from: holder }); + await this.token.delegate(holder, { from: holder }); }); it('reverts if block number >= current block', async function () { From 3aef1f90d21a2f2d153b23370acd3a599f904a57 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 14:54:43 -0400 Subject: [PATCH 184/300] Finished tests --- test/token/ERC721/extensions/ERC721Votes.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 9712e69d21e..6d705c7c80f 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -415,16 +415,16 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const [ t1, t2, t3 ] = await batchInBlock([ - () => this.token.delegate(other1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, NFT1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, NFT2, { from: recipient, gas: 100000 }), + () => this.token.delegate(other1, { from: recipient, gas: 200000 }), + () => this.token.transfer(other2, NFT1, { from: recipient, gas: 200000 }), + () => this.token.transfer(other2, NFT2, { from: recipient, gas: 200000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); }); From 9784e049a49b40e3aee984a6e096f7443e4ab2e8 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 16:24:20 -0400 Subject: [PATCH 185/300] Adding _afterTokenTransfer to base ERC271 contract --- .../extensions/GovernorVotesERC721.sol | 4 +- contracts/token/ERC721/ERC721.sol | 22 ++++ .../token/ERC721/extensions/ERC721Votes.sol | 19 +-- .../ERC721/extensions/ERC721Votes.test.js | 34 ++--- .../extensions/draft-ERC721Permit.test.js | 117 ------------------ 5 files changed, 44 insertions(+), 152 deletions(-) delete mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 9a2a4ac0ee8..383960d9ef1 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC72GovernorVotesERC721.sol) +// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC721GovernorVotesERC721.sol) pragma solidity ^0.8.0; @@ -12,7 +12,7 @@ import "../../utils/math/Math.sol"; * * _Available since v4.3._ */ -abstract contract ERC72GovernorVotesERC721 is Governor { +abstract contract ERC721GovernorVotesERC721 is Governor { ERC721Votes public immutable token; constructor(ERC721Votes tokenAddress) { diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 3c960417dcb..6fba12a2671 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -342,6 +342,8 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { _owners[tokenId] = to; emit Transfer(from, to, tokenId); + + _afterTokenTransfer(from, to, tokenId); } /** @@ -421,4 +423,24 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { address to, uint256 tokenId ) internal virtual {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `tokenId` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `tokenId` tokens have been minted for `to`. + * - when `to` is zero, `tokenId` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual {} } diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 978bcd0787e..18164dc6721 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; -import "./draft-ERC721Permit.sol"; +import "../ERC721.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; @@ -22,7 +22,7 @@ import "../../../utils/cryptography/ECDSA.sol"; * * _Available since v4.2._ */ -abstract contract ERC721Votes is ERC721Permit { +abstract contract ERC721Votes is ERC721 { struct Checkpoint { uint32 fromBlock; uint224 votes; @@ -191,7 +191,7 @@ abstract contract ERC721Votes is ERC721Permit { function _afterTokenTransfer( address from, address to - ) internal virtual { + ) internal virtual override{ _moveVotingPower(delegates(from), delegates(to), 1); } @@ -251,17 +251,4 @@ abstract contract ERC721Votes is ERC721Permit { function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } - - /** - * @dev Moves token from the caller's account to `recipient`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 tokenId) external returns (bool){ - _transfer(_msgSender(), recipient, tokenId); - _afterTokenTransfer(_msgSender(), recipient); - return true; - } } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 6d705c7c80f..1a114c1c6b2 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -299,7 +299,7 @@ contract('ERC721Votes', function (accounts) { }); it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); @@ -310,7 +310,7 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); @@ -324,7 +324,7 @@ contract('ERC721Votes', function (accounts) { it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -339,7 +339,7 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transfer(recipient, supply, { from: holder }); + const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -381,20 +381,20 @@ contract('ERC721Votes', function (accounts) { describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { - await this.token.transfer(recipient, NFT1, { from: holder }); //give an account two tokens for readability - await this.token.transfer(recipient, NFT2, { from: holder }); + await this.token.transferFrom(recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transferFrom(recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - const t2 = await this.token.transfer(other2, NFT1, { from: recipient }); + const t2 = await this.token.transferFrom(other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - const t3 = await this.token.transfer(other2, NFT2, { from: recipient }); + const t3 = await this.token.transferFrom(other2, NFT2, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); @@ -410,19 +410,19 @@ contract('ERC721Votes', function (accounts) { }); it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, NFT1, { from: holder }); - await this.token.transfer(recipient, NFT2, { from: holder }); + await this.token.transferFrom(recipient, NFT1, { from: holder }); + await this.token.transferFrom(recipient, NFT2, { 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, NFT1, { from: recipient, gas: 200000 }), - () => this.token.transfer(other2, NFT2, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(other2, NFT1, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(other2, NFT2, { from: recipient, gas: 200000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); - const t4 = await this.token.transfer(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); @@ -465,13 +465,13 @@ contract('ERC721Votes', function (accounts) { const t1 = await this.token.delegate(other1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t2 = await this.token.transfer(other2, NFT1, { from: holder }); + const t2 = await this.token.transferFrom(other2, NFT1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.transfer(other2, NFT2, { from: holder }); + const t3 = await this.token.transferFrom(other2, NFT2, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transfer(holder, NFT2, { from: other2 }); + const t4 = await this.token.transferFrom(holder, NFT2, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js deleted file mode 100644 index f22c904c724..00000000000 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable */ - -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); -const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; - -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; - -const ERC721PermitMock = artifacts.require('ERC721PermitMock'); - -const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); - -const Permit = [ - { name: 'owner', type: 'address' }, - { name: 'spender', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint256' }, -]; - -contract('ERC721Permit', function (accounts) { - const [ initialHolder, spender, recipient, other ] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - - const initialTokenId = new BN('100'); - - beforeEach(async function () { - this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); - - // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id - // from within the EVM as from the JSON RPC interface. - // See https://github.com/trufflesuite/ganache-core/issues/515 - this.chainId = await this.token.getChainId(); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect( - await this.token.DOMAIN_SEPARATOR(), - ).to.equal( - await domainSeparator(name, version, this.chainId, this.token.address), - ); - }); - - describe('permit', function () { - const wallet = Wallet.generate(); - - const owner = wallet.getAddressString(); - const value = initialTokenId; - const nonce = 0; - const maxDeadline = MAX_UINT256; - - const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ - primaryType: 'Permit', - types: { EIP712Domain, Permit }, - domain: { name, version, chainId, verifyingContract }, - message: { owner, spender, value, nonce, deadline }, - }); - - it('accepts owner signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); - expect(await this.token.getApproved(value)).to.be.equal(spender); - }); - - it('rejects reused signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects other signature', async function () { - const otherWallet = Wallet.generate(); - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects expired permit', async function () { - const deadline = (await time.latest()) - time.duration.weeks(1); - - const data = buildData(this.chainId, this.token.address, deadline); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, deadline, v, r, s), - 'ERC721Permit: expired deadline', - ); - }); - }); -}); From 1a3dc28d30b93df93859548163236869f9f006fb Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 16:30:31 -0400 Subject: [PATCH 186/300] Renaming supplied tokenId on tests --- .../ERC721/extensions/draft-ERC721Permit.sol | 87 ------------------- .../ERC721/extensions/draft-IERC721Permit.sol | 60 ------------- .../ERC721/extensions/ERC721Votes.test.js | 34 ++++---- 3 files changed, 17 insertions(+), 164 deletions(-) delete mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol delete mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol deleted file mode 100644 index c00d15367ab..00000000000 --- a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) - -pragma solidity ^0.8.0; - -import "./draft-IERC721Permit.sol"; -import "./ERC721Enumerable.sol"; -import "../../../utils/cryptography/draft-EIP712.sol"; -import "../../../utils/cryptography/ECDSA.sol"; -import "../../../utils/Counters.sol"; - -/** - * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - * - * _Available since v3.4._ - */ -abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { - using Counters for Counters.Counter; - - mapping(address => Counters.Counter) private _nonces; - - // solhint-disable-next-line var-name-mixedcase - bytes32 private immutable _PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - - /** - * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. - * - * It's a good idea to use the same `name` that is defined as the ERC721 token name. - */ - constructor(string memory name) EIP712(name, "1") {} - - /** - * @dev See {IERC721Permit-permit}. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual override { - require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); - - bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); - - bytes32 hash = _hashTypedDataV4(structHash); - - address signer = ECDSA.recover(hash, v, r, s); - require(signer == owner, "ERC721Permit: invalid signature"); - - _approve(spender, value); - } - - /** - * @dev See {IERC721Permit-nonces}. - */ - function nonces(address owner) public view virtual override returns (uint256) { - return _nonces[owner].current(); - } - - /** - * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view override returns (bytes32) { - return _domainSeparatorV4(); - } - - /** - * @dev "Consume a nonce": return the current value and increment. - * - * _Available since v4.1._ - */ - function _useNonce(address owner) internal virtual returns (uint256 current) { - Counters.Counter storage nonce = _nonces[owner]; - current = nonce.current(); - nonce.increment(); - } -} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol deleted file mode 100644 index 61882f2de0d..00000000000 --- a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - */ -interface IERC721Permit { - /** - * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, - * given ``owner``'s signed approval. - * - * IMPORTANT: The same issues {IERC721-approve} has related to transaction - * ordering also apply here. - * - * Emits an {Approval} event. - * - * Requirements: - * - * - `spender` cannot be the zero address. - * - `deadline` must be a timestamp in the future. - * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` - * over the EIP712-formatted function arguments. - * - the signature must use ``owner``'s current nonce (see {nonces}). - * - * For more information on the signature format, see the - * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP - * section]. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - /** - * @dev Returns the current nonce for `owner`. This value must be - * included whenever a signature is generated for {permit}. - * - * Every successful call to {permit} increases ``owner``'s nonce by one. This - * prevents a signature from being used multiple times. - */ - function nonces(address owner) external view returns (uint256); - - /** - * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view returns (bytes32); -} diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 1a114c1c6b2..21d5587d66f 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -61,7 +61,7 @@ contract('ERC721Votes', function (accounts) { const name = 'My Token'; const symbol = 'MTKN'; const version = '1'; - const supply = new BN('10000000000000000000000000'); + const initalTokenId = new BN('10000000000000000000000000'); beforeEach(async function () { this.token = await ERC721VotesMock.new(name, symbol); @@ -89,7 +89,7 @@ contract('ERC721Votes', function (accounts) { this.token.mint(holder, NFT1); this.token.mint(holder, NFT2); this.token.mint(holder, NFT3); - this.token.mint(holder, supply); + this.token.mint(holder, initalTokenId); await expectRevert( this.token.mint(holder, lastTokenId), @@ -100,7 +100,7 @@ contract('ERC721Votes', function (accounts) { describe('set delegation', function () { describe('call', function () { it('delegation with tokenId', async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, initalTokenId); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -151,7 +151,7 @@ contract('ERC721Votes', function (accounts) { }}); beforeEach(async function () { - await this.token.mint(delegatorAddress, supply); + await this.token.mint(delegatorAddress, initalTokenId); }); it('accept signed delegation', async function () { @@ -257,7 +257,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, initalTokenId); await this.token.delegate(holder, { from: holder }); }); @@ -295,12 +295,12 @@ contract('ERC721Votes', function (accounts) { describe('transfers', function () { beforeEach(async function () { - await this.token.mint(holder, supply); + await this.token.mint(holder, initalTokenId); }); it('no delegation', async function () { - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -310,8 +310,8 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -324,8 +324,8 @@ contract('ERC721Votes', function (accounts) { it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -339,8 +339,8 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, supply, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: supply }); + const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -366,7 +366,7 @@ contract('ERC721Votes', function (accounts) { // 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); + await this.token.mint(holder, initalTokenId); await this.token.mint(holder, NFT1); await this.token.mint(holder, NFT2); await this.token.mint(holder, NFT3); @@ -505,7 +505,7 @@ contract('ERC721Votes', function (accounts) { }); it('returns the latest block if >= last checkpoint block', async function () { - t1 = await this.token.mint(holder, supply); + t1 = await this.token.mint(holder, initalTokenId); await time.advanceBlock(); await time.advanceBlock(); @@ -516,7 +516,7 @@ contract('ERC721Votes', function (accounts) { it('returns zero if < first checkpoint block', async function () { await time.advanceBlock(); - const t1 = await this.token.mint(holder, supply); + const t1 = await this.token.mint(holder, initalTokenId); await time.advanceBlock(); await time.advanceBlock(); From e37f4bf86a3fb6862e70db277c8b7be56040cc56 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 17:45:23 -0400 Subject: [PATCH 187/300] Updating tests based on new contract changes --- contracts/mocks/ERC721PermitMock.sol | 20 -------- contracts/mocks/ERC721VotesMock.sol | 2 +- .../token/ERC721/extensions/ERC721Votes.sol | 50 +++++++++++++++++-- .../ERC721/extensions/ERC721Votes.test.js | 34 ++++++------- 4 files changed, 63 insertions(+), 43 deletions(-) delete mode 100644 contracts/mocks/ERC721PermitMock.sol diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol deleted file mode 100644 index 37a860ef4dc..00000000000 --- a/contracts/mocks/ERC721PermitMock.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../token/ERC721/extensions/draft-ERC721Permit.sol"; - -contract ERC721PermitMock is ERC721Permit { - constructor( - string memory name, - string memory symbol, - address initialAccount, - uint256 tokenId - ) payable ERC721(name, symbol) ERC721Permit(name) { - _mint(initialAccount, tokenId); - } - - function getChainId() external view returns (uint256) { - return block.chainid; - } -} diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 09e40a4ea85..3b7a1bed7a8 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { - constructor(string memory name, string memory symbol) ERC721(name, symbol) ERC721Permit(name) {} + constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {} function mint(address account, uint256 tokenId) public { _mint(account, tokenId); diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 18164dc6721..00a5e8707c0 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -4,9 +4,11 @@ pragma solidity ^0.8.0; import "../ERC721.sol"; +import "../../../utils/Counters.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; /** * @dev Extension of ERC721 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. @@ -22,19 +24,29 @@ import "../../../utils/cryptography/ECDSA.sol"; * * _Available since v4.2._ */ -abstract contract ERC721Votes is ERC721 { +abstract contract ERC721Votes is ERC721, EIP712 { + using Counters for Counters.Counter; + struct Checkpoint { uint32 fromBlock; uint224 votes; } - + uint256 _totalSupply; bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); mapping(address => address) private _delegates; + mapping(address => Counters.Counter) private _nonces; mapping(address => Checkpoint[]) private _checkpoints; Checkpoint[] private _totalSupplyCheckpoints; + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC721 token name. + + constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ + /** * @dev Emitted when an account changes their delegate. */ @@ -169,7 +181,8 @@ abstract contract ERC721Votes is ERC721 { */ function _mint(address account, uint256 tokenId) internal virtual override { super._mint(account, tokenId); - require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + _totalSupply += 1; + require(_totalSupply <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } @@ -179,7 +192,7 @@ abstract contract ERC721Votes is ERC721 { */ function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); - + _totalSupply -= 1; _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); } @@ -190,7 +203,8 @@ abstract contract ERC721Votes is ERC721 { */ function _afterTokenTransfer( address from, - address to + address to, + uint256 tokenId ) internal virtual override{ _moveVotingPower(delegates(from), delegates(to), 1); } @@ -244,6 +258,32 @@ abstract contract ERC721Votes is ERC721 { } } + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } + + /** + * @dev Returns an address nonce. + */ + function nonces(address owner) public view virtual returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev Returns DOMAIN_SEPARATOR. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparatorV4(); + } + function _add(uint256 a, uint256 b) private pure returns (uint256) { return a + b; } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 21d5587d66f..a5b9cd16198 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -299,7 +299,7 @@ contract('ERC721Votes', function (accounts) { }); it('no delegation', async function () { - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); @@ -310,7 +310,7 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); @@ -324,7 +324,7 @@ contract('ERC721Votes', function (accounts) { it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -339,7 +339,7 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); - const { receipt } = await this.token.transferFrom(recipient, initalTokenId, { from: holder }); + const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); @@ -381,20 +381,20 @@ contract('ERC721Votes', function (accounts) { describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { - await this.token.transferFrom(recipient, NFT1, { from: holder }); //give an account two tokens for readability - await this.token.transferFrom(recipient, NFT2, { from: holder }); + await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - const t2 = await this.token.transferFrom(other2, NFT1, { from: recipient }); + const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - const t3 = await this.token.transferFrom(other2, NFT2, { from: recipient }); + const t3 = await this.token.transferFrom(recipient, other2, NFT2, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); @@ -410,19 +410,19 @@ contract('ERC721Votes', function (accounts) { }); it('does not add more than one checkpoint in a block', async function () { - await this.token.transferFrom(recipient, NFT1, { from: holder }); - await this.token.transferFrom(recipient, NFT2, { from: holder }); + await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); + await this.token.transferFrom(holder, recipient, NFT2, { 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.transferFrom(other2, NFT1, { from: recipient, gas: 200000 }), - () => this.token.transferFrom(other2, NFT2, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(recipient, other2, NFT1, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(recipient, other2, NFT2, { from: recipient, gas: 200000 }), ]); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); - const t4 = await this.token.transferFrom(recipient, NFT3, { from: holder }); + const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); @@ -465,13 +465,13 @@ contract('ERC721Votes', function (accounts) { const t1 = await this.token.delegate(other1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t2 = await this.token.transferFrom(other2, NFT1, { from: holder }); + const t2 = await this.token.transferFrom(holder, other2, NFT1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t3 = await this.token.transferFrom(other2, NFT2, { from: holder }); + const t3 = await this.token.transferFrom(holder, other2, NFT2, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); - const t4 = await this.token.transferFrom(holder, NFT2, { from: other2 }); + const t4 = await this.token.transferFrom(other2, holder, NFT2, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); From f275406443a814377f1dccb0c5f2f0974b281cd9 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 20:15:54 -0400 Subject: [PATCH 188/300] Updating execution order inside of mint --- contracts/token/ERC721/extensions/ERC721Votes.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 00a5e8707c0..7871282643b 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -180,10 +180,11 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been increased. */ function _mint(address account, uint256 tokenId) internal virtual override { + require(_totalSupply+1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + super._mint(account, tokenId); _totalSupply += 1; - require(_totalSupply <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } From 6a9caf08e68a239a070275ac6a66e31535e74865 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 16:25:14 -0400 Subject: [PATCH 189/300] Adding Mocks for testing and integrating nft minting to current workflow test --- .../extensions/GovernorVotesERC721.sol | 2 +- contracts/mocks/GovernorERC721Mock.sol | 41 +++++++++ test/governance/GovernorWorkflow.behavior.js | 4 +- .../extensions/GovernorERC721.test.js | 91 +++++++++++++++++++ 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 contracts/mocks/GovernorERC721Mock.sol create mode 100644 test/governance/extensions/GovernorERC721.test.js diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 383960d9ef1..2e3079fc243 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -12,7 +12,7 @@ import "../../utils/math/Math.sol"; * * _Available since v4.3._ */ -abstract contract ERC721GovernorVotesERC721 is Governor { +abstract contract GovernorVotesERC721 is Governor { ERC721Votes public immutable token; constructor(ERC721Votes tokenAddress) { diff --git a/contracts/mocks/GovernorERC721Mock.sol b/contracts/mocks/GovernorERC721Mock.sol new file mode 100644 index 00000000000..7508f168334 --- /dev/null +++ b/contracts/mocks/GovernorERC721Mock.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../governance/extensions/GovernorCountingSimple.sol"; +import "../governance/extensions/GovernorVotesERC721.sol"; + +contract GovernorERC721Mock is GovernorVotesERC721, GovernorCountingSimple { + constructor(string memory name_, ERC721Votes token_) Governor(name_) GovernorVotesERC721(token_) {} + + 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; + } + + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 salt + ) public returns (uint256 proposalId) { + return _cancel(targets, values, calldatas, salt); + } + + function getVotes(address account, uint256 blockNumber) + public + view + virtual + override(IGovernor, GovernorVotesERC721) + returns (uint256) + { + return super.getVotes(account, blockNumber); + } +} diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js index 70319cd44d3..e8e2416b19e 100644 --- a/test/governance/GovernorWorkflow.behavior.js +++ b/test/governance/GovernorWorkflow.behavior.js @@ -31,7 +31,9 @@ function runGovernorWorkflow () { for (const voter of this.settings.voters) { if (voter.weight) { await this.token.transfer(voter.voter, voter.weight, { from: this.settings.tokenHolder }); - } + }else if(voter.nftWeight){ + await this.token.transferFrom(this.settings.tokenHolder, voter.voter, voter.nftWeight, { from: this.settings.tokenHolder }); + } } } diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js new file mode 100644 index 00000000000..845d5c20d21 --- /dev/null +++ b/test/governance/extensions/GovernorERC721.test.js @@ -0,0 +1,91 @@ +const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const Enums = require('../../helpers/enums'); + +const { + runGovernorWorkflow, +} = require('./../GovernorWorkflow.behavior'); + +const Token = artifacts.require('ERC721VotesMock'); +const Governor = artifacts.require('GovernorERC721Mock'); +const CallReceiver = artifacts.require('CallReceiverMock'); + +contract('GovernorERC721Mock', function (accounts) { + const [ owner, voter1, voter2, voter3, voter4 ] = accounts; + + const name = 'OZ-Governor'; + const tokenName = 'MockNFToken'; + const tokenSymbol = 'MTKN'; + const initalTokenId = web3.utils.toWei('100'); + const NFT1 = web3.utils.toWei('10'); + const NFT2 = web3.utils.toWei('20'); + const NFT3 = web3.utils.toWei('30'); + + beforeEach(async function () { + this.owner = owner; + this.token = await Token.new(tokenName, tokenSymbol); + this.mock = await Governor.new(name, this.token.address); + this.receiver = await CallReceiver.new(); + await this.token.mint(owner, initalTokenId); + await this.token.mint(owner, NFT1); + await this.token.mint(owner, NFT2); + await this.token.mint(owner, NFT3); + + await this.token.delegate(voter1, { from: voter1 }); + await this.token.delegate(voter2, { from: voter2 }); + await this.token.delegate(voter3, { from: voter3 }); + await this.token.delegate(voter4, { from: voter4 }); + }); + + 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('4'); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16'); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + }); + + describe.only('voting with ERC721 token', function () { + beforeEach(async function () { + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, nftWeight: initalTokenId, support: Enums.VoteType.For }, + { voter: voter2, nftWeight: NFT1, support: Enums.VoteType.For }, + { voter: voter3, nftWeight: NFT2, support: Enums.VoteType.Against }, + { voter: voter4, nftWeight: NFT3, support: Enums.VoteType.Abstain }, + ] + } + }); + + afterEach(async function () { + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter3)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, voter4)).to.be.equal(true); + + this.receipts.castVote.filter(Boolean).forEach(vote => { + const { voter } = vote.logs.find(Boolean).args; + expectEvent( + vote, + 'VoteCast', + this.settings.voters.find(({ address }) => address === voter), + ); + }); + await this.mock.proposalVotes(this.id).then(result => { + for (const [key, value] of Object.entries(Enums.VoteType)) { + expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( + Object.values(this.settings.voters).filter(({ support }) => support === value).length.toString() + ); + } + }); + }); + runGovernorWorkflow(); + }); +}); From 5a58fc2e6710f037a8dfddafde5a87aedf76fafb Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 17:02:56 -0400 Subject: [PATCH 190/300] Implementing override test --- .../extensions/GovernorERC721.test.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 845d5c20d21..4f81055800a 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -64,20 +64,22 @@ contract('GovernorERC721Mock', function (accounts) { }); afterEach(async function () { - expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.id, voter2)).to.be.equal(true); - expect(await this.mock.hasVoted(this.id, voter3)).to.be.equal(true); - expect(await this.mock.hasVoted(this.id, voter4)).to.be.equal(true); + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); - this.receipts.castVote.filter(Boolean).forEach(vote => { + for(const vote of this.receipts.castVote.filter(Boolean)){ const { voter } = vote.logs.find(Boolean).args; + + expect(await this.mock.hasVoted(this.id, voter)).to.be.equal(true); + expectEvent( vote, 'VoteCast', this.settings.voters.find(({ address }) => address === voter), ); - }); + + expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); + } + await this.mock.proposalVotes(this.id).then(result => { for (const [key, value] of Object.entries(Enums.VoteType)) { expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( @@ -86,6 +88,8 @@ contract('GovernorERC721Mock', function (accounts) { } }); }); + runGovernorWorkflow(); + }); }); From e07dfa69e3d2baf366fe90cb0eb63a05914a0b8d Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 17:47:26 -0400 Subject: [PATCH 191/300] Removing .only from tests --- test/governance/extensions/GovernorERC721.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 4f81055800a..ee8ad8399da 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -44,7 +44,7 @@ contract('GovernorERC721Mock', function (accounts) { expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); }); - describe.only('voting with ERC721 token', function () { + describe('voting with ERC721 token', function () { beforeEach(async function () { this.settings = { proposal: [ From 0bc0ab5d033c58efebc56e5bee442c4f7c755bd9 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 18:05:24 -0400 Subject: [PATCH 192/300] Updating contracts READMEs --- contracts/governance/README.adoc | 4 ++++ contracts/token/ERC721/README.adoc | 2 ++ 2 files changed, 6 insertions(+) diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index d388f4e3a42..ef5d416c012 100644 --- a/contracts/governance/README.adoc +++ b/contracts/governance/README.adoc @@ -22,6 +22,8 @@ Votes modules determine the source of voting power, and sometimes quorum number. * {GovernorVotes}: Extracts voting weight from an {ERC20Votes} token. +* {GovernorVotesERC721}: Extracts voting weight from 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. @@ -64,6 +66,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you {{GovernorVotesQuorumFraction}} +{{GovernorVotesERC721}} + {{GovernorVotesComp}} === Extensions diff --git a/contracts/token/ERC721/README.adoc b/contracts/token/ERC721/README.adoc index f1122c53a99..51089e1627c 100644 --- a/contracts/token/ERC721/README.adoc +++ b/contracts/token/ERC721/README.adoc @@ -41,6 +41,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel {{ERC721URIStorage}} +{{ERC721Votes}} + == Presets These contracts are preconfigured combinations of the above features. They can be used through inheritance or as models to copy and paste their source code. From 61523ed47b7d20145f0f6fc0910141aca8576a10 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 18:57:08 -0400 Subject: [PATCH 193/300] Governance adocs update --- contracts/mocks/ERC721VotesMock.sol | 2 +- contracts/mocks/UserTOkenerc721Mock.sol | 32 +++++++++++++ .../token/ERC721/extensions/ERC721Votes.sol | 3 +- docs/modules/ROOT/pages/erc721.adoc | 2 +- docs/modules/ROOT/pages/governance.adoc | 45 ++++++++++++++++++- 5 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 3b7a1bed7a8..bde65e5a5ff 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { - constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {} + constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} function mint(address account, uint256 tokenId) public { _mint(account, tokenId); diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol new file mode 100644 index 00000000000..b3874e66da5 --- /dev/null +++ b/contracts/mocks/UserTOkenerc721Mock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; + +contract MyToken is ERC721, EIP712, ERC721Votes { + constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._burn(account, amount); + } +} \ No newline at end of file diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 7871282643b..39c03934028 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -44,8 +44,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. * * It's a good idea to use the same `name` that is defined as the ERC721 token name. - - constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ + */ /** * @dev Emitted when an account changes their delegate. diff --git a/docs/modules/ROOT/pages/erc721.adoc b/docs/modules/ROOT/pages/erc721.adoc index 8d28fad2e6e..14dbdc97606 100644 --- a/docs/modules/ROOT/pages/erc721.adoc +++ b/docs/modules/ROOT/pages/erc721.adoc @@ -1,6 +1,6 @@ = ERC721 -We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate* or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. +We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate*, *voting rights*, or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. ERC721 is a more complex standard than ERC20, with multiple optional extensions, and is split across a number of contracts. The OpenZeppelin Contracts provide flexibility regarding how these are combined, along with custom useful extensions. Check out the xref:api:token/ERC721.adoc[API Reference] to learn more about these. diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 027301c1ba5..a262a75af38 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -14,6 +14,10 @@ This governance protocol is generally implemented in a special-purpose contract OpenZeppelin’s Governor system was designed with a concern for compatibility with existing systems that were based on Compound’s GovernorAlpha and GovernorBravo. Because of this, you will find that many modules are presented in two variants, one of which is built for compatibility with those systems. +=== ERC721Votes + +The ERC721 extension to keep track of votes and vote delegation is one such case. + === ERC20Votes & ERC20VotesComp The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. @@ -119,11 +123,48 @@ contract MyToken is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper { } ``` +If your project requires The voting power of each account in our governance setup will be determined by an ERC721 token. The token has to implement the ERC721Votes extension. This extension will keep track of historical balances so that voting power is retrieved from past snapshots rather than current balance, which is an important protection that prevents double voting. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; + +contract MyToken is ERC721, EIP712, ERC721Votes { + constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._burn(account, amount); + } +} +``` + NOTE: Voting power could be determined in different ways: multiple ERC20 tokens, ERC721 tokens, sybil resistant identities, etc. All of these options are potentially supported by writing a custom Votes module for your Governor. === Governor -Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, and 3) what options people have when casting a vote and how those votes are counted. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. +Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, 3) what options people have when casting a vote and how those votes are counted, and 4)what type of token should be use to vote. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. For 1) we will use the GovernorVotes module, which hooks to an ERC20Votes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. @@ -131,6 +172,8 @@ For 2) we will use GovernorVotesQuorumFraction which works together with ERC20Vo For 3) we will use GovernorCountingSimple, a module that offers 3 options to voters: For, Against, and Abstain, and where only For and Abstain votes are counted towards quorum. +For 4) we will use the GovernorVotesERC721 module, which hooks to an ERC721Votes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. + Besides these modules, Governor itself has some parameters we must set. votingDelay: How long after a proposal is created should voting power be fixed. A large voting delay gives users time to unstake tokens if necessary. From f82873ef857aaf440189dc2c91bf8c4b9972ac12 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 14:13:44 -0400 Subject: [PATCH 194/300] Removing test contract --- contracts/mocks/UserTOkenerc721Mock.sol | 32 ------------------------- 1 file changed, 32 deletions(-) delete mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol deleted file mode 100644 index b3874e66da5..00000000000 --- a/contracts/mocks/UserTOkenerc721Mock.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "@openzeppelin/contracts/token/ERC721.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; - -contract MyToken is ERC721, EIP712, ERC721Votes { - constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._afterTokenTransfer(from, to, amount); - } - - function _mint(address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._mint(to, amount); - } - - function _burn(address account, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._burn(account, amount); - } -} \ No newline at end of file From 4f0f39e73e80a0c1fe84b259784e1d4e3ff0ae88 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sat, 30 Oct 2021 20:49:29 -0400 Subject: [PATCH 195/300] Initial contracts creation --- .../extensions/GovernorVotesERC721.sol | 2 +- .../ERC721/extensions/draft-ERC721Permit.sol | 87 +++++++++++++++++++ .../ERC721/extensions/draft-IERC721Permit.sol | 60 +++++++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol create mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 2e3079fc243..c2472a2e4e2 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC721GovernorVotesERC721.sol) +// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC72GovernorVotesERC721.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol new file mode 100644 index 00000000000..c00d15367ab --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) + +pragma solidity ^0.8.0; + +import "./draft-IERC721Permit.sol"; +import "./ERC721Enumerable.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; +import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/Counters.sol"; + +/** + * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * _Available since v3.4._ + */ +abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { + using Counters for Counters.Counter; + + mapping(address => Counters.Counter) private _nonces; + + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC721 token name. + */ + constructor(string memory name) EIP712(name, "1") {} + + /** + * @dev See {IERC721Permit-permit}. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); + + bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + + bytes32 hash = _hashTypedDataV4(structHash); + + address signer = ECDSA.recover(hash, v, r, s); + require(signer == owner, "ERC721Permit: invalid signature"); + + _approve(spender, value); + } + + /** + * @dev See {IERC721Permit-nonces}. + */ + function nonces(address owner) public view virtual override returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } +} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol new file mode 100644 index 00000000000..61882f2de0d --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + */ +interface IERC721Permit { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC721-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} From acc4aa7089825ed76b868b1bdea2573f6c9a3a11 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 09:28:35 -0400 Subject: [PATCH 196/300] creating permit tests --- contracts/mocks/ERC721PermitMock.sol | 20 ++ .../ERC721/extensions/ERC721Votes.test.js | 233 +++++++++++++++++- .../extensions/draft-ERC721Permit.test.js | 117 +++++++++ 3 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 contracts/mocks/ERC721PermitMock.sol create mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol new file mode 100644 index 00000000000..37a860ef4dc --- /dev/null +++ b/contracts/mocks/ERC721PermitMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC721/extensions/draft-ERC721Permit.sol"; + +contract ERC721PermitMock is ERC721Permit { + constructor( + string memory name, + string memory symbol, + address initialAccount, + uint256 tokenId + ) payable ERC721(name, symbol) ERC721Permit(name) { + _mint(initialAccount, tokenId); + } + + function getChainId() external view returns (uint256) { + return block.chainid; + } +} diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index a5b9cd16198..3d925cc89b5 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -14,7 +14,6 @@ const queue = promisify(setImmediate); const ERC721VotesMock = artifacts.require('ERC721VotesMock'); const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); -const { Console } = require('console'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -55,6 +54,7 @@ async function batchInBlock (txs) { contract('ERC721Votes', function (accounts) { const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; +<<<<<<< HEAD const NFT1 = new BN('10'); const NFT2 = new BN('20'); const NFT3 = new BN('30'); @@ -62,6 +62,13 @@ contract('ERC721Votes', function (accounts) { const symbol = 'MTKN'; const version = '1'; const initalTokenId = new BN('10000000000000000000000000'); +======= + + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; + const supply = new BN('10000000000000000000000000'); +>>>>>>> creating permit tests beforeEach(async function () { this.token = await ERC721VotesMock.new(name, symbol); @@ -85,6 +92,7 @@ contract('ERC721Votes', function (accounts) { }); it('minting restriction', async function () { +<<<<<<< HEAD const lastTokenId = new BN('2').pow(new BN('224')); this.token.mint(holder, NFT1); this.token.mint(holder, NFT2); @@ -93,14 +101,24 @@ contract('ERC721Votes', function (accounts) { await expectRevert( this.token.mint(holder, lastTokenId), +======= + const amount = new BN('2').pow(new BN('224')); + await expectRevert( + this.token.mint(holder, amount), +>>>>>>> creating permit tests 'ERC721Votes: total supply risks overflowing votes', ); }); describe('set delegation', function () { describe('call', function () { +<<<<<<< HEAD it('delegation with tokenId', async function () { await this.token.mint(holder, initalTokenId); +======= + it('delegation with balance', async function () { + await this.token.mint(holder, supply); +>>>>>>> creating permit tests expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -112,11 +130,16 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '0', +<<<<<<< HEAD newBalance: '1', +======= + newBalance: supply, +>>>>>>> creating permit tests }); expect(await this.token.delegates(holder)).to.be.equal(holder); +<<<<<<< HEAD expect(await this.token.getVotes(holder)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); @@ -124,6 +147,15 @@ contract('ERC721Votes', function (accounts) { }); it('delegation without tokenId', async function () { +======= + expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); + }); + + it('delegation without balance', async function () { +>>>>>>> creating permit tests expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -151,7 +183,11 @@ contract('ERC721Votes', function (accounts) { }}); beforeEach(async function () { +<<<<<<< HEAD await this.token.mint(delegatorAddress, initalTokenId); +======= + await this.token.mint(delegatorAddress, supply); +>>>>>>> creating permit tests }); it('accept signed delegation', async function () { @@ -175,15 +211,26 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: delegatorAddress, previousBalance: '0', +<<<<<<< HEAD newBalance: '1', +======= + newBalance: supply, +>>>>>>> creating permit tests }); expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); +<<<<<<< HEAD expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1'); +======= + expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); +>>>>>>> creating permit tests }); it('rejects reused signature', async function () { @@ -257,7 +304,11 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { +<<<<<<< HEAD await this.token.mint(holder, initalTokenId); +======= + await this.token.mint(holder, supply); +>>>>>>> creating permit tests await this.token.delegate(holder, { from: holder }); }); @@ -272,35 +323,61 @@ contract('ERC721Votes', function (accounts) { }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, +<<<<<<< HEAD previousBalance: '1', +======= + previousBalance: supply, +>>>>>>> creating permit tests newBalance: '0', }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holderDelegatee, previousBalance: '0', +<<<<<<< HEAD newBalance: '1', +======= + newBalance: supply, +>>>>>>> creating permit tests }); expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); +<<<<<<< HEAD expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); +======= + expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); +>>>>>>> creating permit tests }); }); describe('transfers', function () { beforeEach(async function () { +<<<<<<< HEAD await this.token.mint(holder, initalTokenId); }); it('no delegation', async function () { const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); +======= + 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' }); +>>>>>>> creating permit tests expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -310,22 +387,37 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); +<<<<<<< HEAD const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); +======= + 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) }); +>>>>>>> creating permit tests const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); expect(receipt.logs.filter(({ event }) => event == 'DelegateVotesChanged').every(({ logIndex }) => transferLogIndex < logIndex)).to.be.equal(true); +<<<<<<< HEAD this.holderVotes = '0'; +======= + this.holderVotes = supply.subn(1); +>>>>>>> creating permit tests this.recipientVotes = '0'; }); it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); +<<<<<<< HEAD const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); +======= + const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); +>>>>>>> creating permit tests expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -339,15 +431,25 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); +<<<<<<< HEAD const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); +======= + 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) }); +>>>>>>> creating permit tests 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); +<<<<<<< HEAD this.holderVotes = '0'; +======= + this.holderVotes = supply.subn(1); +>>>>>>> creating permit tests this.recipientVotes = '1'; }); @@ -366,27 +468,40 @@ contract('ERC721Votes', function (accounts) { // 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 () { +<<<<<<< HEAD await this.token.mint(holder, initalTokenId); await this.token.mint(holder, NFT1); await this.token.mint(holder, NFT2); await this.token.mint(holder, NFT3); +======= + await this.token.mint(holder, supply); +>>>>>>> creating permit tests }); describe('balanceOf', function () { it('grants to initial account', async function () { +<<<<<<< HEAD expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('4'); +======= + expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); +>>>>>>> creating permit tests }); }); describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { +<<<<<<< HEAD await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); +======= + await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability +>>>>>>> creating permit tests expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); +<<<<<<< HEAD const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); @@ -425,6 +540,47 @@ contract('ERC721Votes', function (accounts) { const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); +======= + + const t2 = await this.token.transfer(other2, 10, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + + const t3 = await this.token.transfer(other2, 10, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); + + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '100' ]); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '90' ]); + expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '80' ]); + expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); + + await time.advanceBlock(); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); + }); + + it('does not add more than one checkpoint in a block', async function () { + await this.token.transfer(recipient, '100', { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const [ t1, t2, t3 ] = await batchInBlock([ + () => this.token.delegate(other1, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + ]); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); + // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check + // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); +>>>>>>> creating permit tests }); }); @@ -445,8 +601,13 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); +<<<<<<< HEAD expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('4'); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); +======= + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); +>>>>>>> creating permit tests }); it('returns zero if < first checkpoint block', async function () { @@ -456,6 +617,7 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); +<<<<<<< HEAD expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); }); @@ -484,6 +646,34 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3'); expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); +======= + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.transfer(holder, 20, { from: other2 }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); +>>>>>>> creating permit tests }); }); }); @@ -505,22 +695,36 @@ contract('ERC721Votes', function (accounts) { }); it('returns the latest block if >= last checkpoint block', async function () { +<<<<<<< HEAD t1 = await this.token.mint(holder, initalTokenId); +======= + t1 = await this.token.mint(holder, supply); +>>>>>>> creating permit tests await time.advanceBlock(); await time.advanceBlock(); +<<<<<<< HEAD expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); +======= + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); +>>>>>>> creating permit tests }); it('returns zero if < first checkpoint block', async function () { await time.advanceBlock(); +<<<<<<< HEAD const t1 = await this.token.mint(holder, initalTokenId); +======= + const t1 = await this.token.mint(holder, supply); +>>>>>>> creating permit tests await time.advanceBlock(); await time.advanceBlock(); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); +<<<<<<< HEAD expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); @@ -538,10 +742,27 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); const t5 = await this.token.mint(holder, NFT3); +======= + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.mint(holder, supply); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.burn(10); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.burn(10); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.mint(holder, 20); +>>>>>>> creating permit tests await time.advanceBlock(); await time.advanceBlock(); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); +<<<<<<< HEAD expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); @@ -552,6 +773,16 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); +======= + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); +>>>>>>> creating permit tests }); }); }); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js new file mode 100644 index 00000000000..7a7b8cf5d83 --- /dev/null +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -0,0 +1,117 @@ +/* eslint-disable */ + +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const ERC721PermitMock = artifacts.require('ERC721PermitMock'); + +const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); + +const Permit = [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, +]; + +contract('ERC721Permit', function (accounts) { + const [ initialHolder, spender, recipient, other ] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; + + const initialTokenId = new BN('100'); + + beforeEach(async function () { + this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); + + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = await this.token.getChainId(); + }); + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect( + await this.token.DOMAIN_SEPARATOR(), + ).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address), + ); + }); + + describe.only('permit', function () { + const wallet = Wallet.generate(); + + const owner = wallet.getAddressString(); + const value = initialTokenId; + const nonce = 0; + const maxDeadline = MAX_UINT256; + + const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ + primaryType: 'Permit', + types: { EIP712Domain, Permit }, + domain: { name, version, chainId, verifyingContract }, + message: { owner, spender, value, nonce, deadline }, + }); + + it('accepts owner signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); + expect(await this.token.getApproved(value)).to.be.equal(spender); + }); + + it('rejects reused signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects other signature', async function () { + const otherWallet = Wallet.generate(); + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects expired permit', async function () { + const deadline = (await time.latest()) - time.duration.weeks(1); + + const data = buildData(this.chainId, this.token.address, deadline); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, deadline, v, r, s), + 'ERC721Permit: expired deadline', + ); + }); + }); +}); From 82e313bc59fea8785f6842fa4f2fdc9796315a77 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 10:26:22 -0400 Subject: [PATCH 197/300] Fixing checkpoints count --- .../token/ERC721/extensions/ERC721Votes.sol | 8 +- .../ERC721/extensions/ERC721Votes.test.js | 312 +++--------------- .../extensions/draft-ERC721Permit.test.js | 2 +- 3 files changed, 44 insertions(+), 278 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 39c03934028..6a7ef27dbec 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -8,7 +8,6 @@ import "../../../utils/Counters.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; -import "../../../utils/cryptography/draft-EIP712.sol"; /** * @dev Extension of ERC721 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. @@ -179,11 +178,9 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been increased. */ function _mint(address account, uint256 tokenId) internal virtual override { - require(_totalSupply+1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - super._mint(account, tokenId); - _totalSupply += 1; - + require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } @@ -192,7 +189,6 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); - _totalSupply -= 1; _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 3d925cc89b5..81397dda8aa 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -53,22 +53,15 @@ async function batchInBlock (txs) { } contract('ERC721Votes', function (accounts) { - const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; -<<<<<<< HEAD + const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; + const NFT0 = new BN('10000000000000000000000000'); const NFT1 = new BN('10'); const NFT2 = new BN('20'); - const NFT3 = new BN('30'); + const NFT3 = new BN('30'); + const NFT4 = new BN('40'); const name = 'My Token'; const symbol = 'MTKN'; const version = '1'; - const initalTokenId = new BN('10000000000000000000000000'); -======= - - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - const supply = new BN('10000000000000000000000000'); ->>>>>>> creating permit tests beforeEach(async function () { this.token = await ERC721VotesMock.new(name, symbol); @@ -92,33 +85,23 @@ contract('ERC721Votes', function (accounts) { }); it('minting restriction', async function () { -<<<<<<< HEAD const lastTokenId = new BN('2').pow(new BN('224')); this.token.mint(holder, NFT1); this.token.mint(holder, NFT2); this.token.mint(holder, NFT3); - this.token.mint(holder, initalTokenId); + this.token.mint(holder, NFT0); + this.token.mint(holder, NFT4); await expectRevert( this.token.mint(holder, lastTokenId), -======= - const amount = new BN('2').pow(new BN('224')); - await expectRevert( - this.token.mint(holder, amount), ->>>>>>> creating permit tests 'ERC721Votes: total supply risks overflowing votes', ); }); describe('set delegation', function () { describe('call', function () { -<<<<<<< HEAD - it('delegation with tokenId', async function () { - await this.token.mint(holder, initalTokenId); -======= - it('delegation with balance', async function () { - await this.token.mint(holder, supply); ->>>>>>> creating permit tests + it('delegation with tokens', async function () { + await this.token.mint(holder, NFT0); expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -130,32 +113,18 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '0', -<<<<<<< HEAD newBalance: '1', -======= - newBalance: supply, ->>>>>>> creating permit tests }); expect(await this.token.delegates(holder)).to.be.equal(holder); -<<<<<<< HEAD expect(await this.token.getVotes(holder)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); }); - it('delegation without tokenId', async function () { -======= - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); - }); - - it('delegation without balance', async function () { ->>>>>>> creating permit tests + it('delegation without tokens', async function () { expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.token.delegate(holder, { from: holder }); @@ -183,11 +152,7 @@ contract('ERC721Votes', function (accounts) { }}); beforeEach(async function () { -<<<<<<< HEAD - await this.token.mint(delegatorAddress, initalTokenId); -======= - await this.token.mint(delegatorAddress, supply); ->>>>>>> creating permit tests + await this.token.mint(delegatorAddress, NFT0); }); it('accept signed delegation', async function () { @@ -211,26 +176,15 @@ contract('ERC721Votes', function (accounts) { expectEvent(receipt, 'DelegateVotesChanged', { delegate: delegatorAddress, previousBalance: '0', -<<<<<<< HEAD newBalance: '1', -======= - newBalance: supply, ->>>>>>> creating permit tests }); expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); -<<<<<<< HEAD expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1'); -======= - expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); ->>>>>>> creating permit tests }); it('rejects reused signature', async function () { @@ -304,11 +258,7 @@ contract('ERC721Votes', function (accounts) { describe('change delegation', function () { beforeEach(async function () { -<<<<<<< HEAD - await this.token.mint(holder, initalTokenId); -======= - await this.token.mint(holder, supply); ->>>>>>> creating permit tests + await this.token.mint(holder, NFT0); await this.token.delegate(holder, { from: holder }); }); @@ -323,61 +273,35 @@ contract('ERC721Votes', function (accounts) { }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, -<<<<<<< HEAD previousBalance: '1', -======= - previousBalance: supply, ->>>>>>> creating permit tests newBalance: '0', }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holderDelegatee, previousBalance: '0', -<<<<<<< HEAD newBalance: '1', -======= - newBalance: supply, ->>>>>>> creating permit tests }); expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); -<<<<<<< HEAD expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); -======= - expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); ->>>>>>> creating permit tests }); }); describe('transfers', function () { beforeEach(async function () { -<<<<<<< HEAD - await this.token.mint(holder, initalTokenId); - }); - - it('no delegation', async function () { - const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); -======= - await this.token.mint(holder, supply); + await this.token.mint(holder, NFT0); }); it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); ->>>>>>> creating permit tests + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); this.holderVotes = '0'; @@ -387,37 +311,22 @@ contract('ERC721Votes', function (accounts) { it('sender delegation', async function () { await this.token.delegate(holder, { from: holder }); -<<<<<<< HEAD - const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); -======= - 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) }); ->>>>>>> creating permit tests const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); expect(receipt.logs.filter(({ event }) => event == 'DelegateVotesChanged').every(({ logIndex }) => transferLogIndex < logIndex)).to.be.equal(true); -<<<<<<< HEAD this.holderVotes = '0'; -======= - this.holderVotes = supply.subn(1); ->>>>>>> creating permit tests this.recipientVotes = '0'; }); it('receiver delegation', async function () { await this.token.delegate(recipient, { from: recipient }); -<<<<<<< HEAD - const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); -======= - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); ->>>>>>> creating permit tests + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); @@ -431,25 +340,15 @@ contract('ERC721Votes', function (accounts) { await this.token.delegate(holder, { from: holder }); await this.token.delegate(recipient, { from: recipient }); -<<<<<<< HEAD - const { receipt } = await this.token.transferFrom(holder, recipient, initalTokenId, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: initalTokenId }); + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); -======= - 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) }); ->>>>>>> creating permit tests 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); -<<<<<<< HEAD this.holderVotes = '0'; -======= - this.holderVotes = supply.subn(1); ->>>>>>> creating permit tests this.recipientVotes = '1'; }); @@ -468,40 +367,27 @@ contract('ERC721Votes', function (accounts) { // 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 () { -<<<<<<< HEAD - await this.token.mint(holder, initalTokenId); + await this.token.mint(holder, NFT0); await this.token.mint(holder, NFT1); await this.token.mint(holder, NFT2); await this.token.mint(holder, NFT3); -======= - await this.token.mint(holder, supply); ->>>>>>> creating permit tests }); describe('balanceOf', function () { it('grants to initial account', async function () { -<<<<<<< HEAD expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('4'); -======= - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); ->>>>>>> creating permit tests }); }); describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { -<<<<<<< HEAD await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); -======= - await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability ->>>>>>> creating permit tests expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); -<<<<<<< HEAD const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); @@ -540,47 +426,6 @@ contract('ERC721Votes', function (accounts) { const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); -======= - - const t2 = await this.token.transfer(other2, 10, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - - const t3 = await this.token.transfer(other2, 10, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '100' ]); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '90' ]); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '80' ]); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); - - await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); - }); - - it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, '100', { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - - const [ t1, t2, t3 ] = await batchInBlock([ - () => this.token.delegate(other1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - ]); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '80' ]); - // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '100' ]); ->>>>>>> creating permit tests }); }); @@ -601,13 +446,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); -<<<<<<< HEAD expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('4'); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); -======= - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); ->>>>>>> creating permit tests }); it('returns zero if < first checkpoint block', async function () { @@ -617,7 +457,6 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); -<<<<<<< HEAD expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); }); @@ -646,86 +485,44 @@ contract('ERC721Votes', function (accounts) { expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3'); expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); -======= - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 20, { from: other2 }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); ->>>>>>> creating permit tests }); }); }); - describe('getPastTotalSupply', function () { + describe('getPastVotingPower', 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), + this.token.getPastVotingPower(5e10), 'ERC721Votes: block not yet mined', ); }); it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(0)).to.be.bignumber.equal('0'); }); it('returns the latest block if >= last checkpoint block', async function () { -<<<<<<< HEAD - t1 = await this.token.mint(holder, initalTokenId); -======= - t1 = await this.token.mint(holder, supply); ->>>>>>> creating permit tests + t1 = await this.token.mint(holder, NFT0); await time.advanceBlock(); await time.advanceBlock(); -<<<<<<< HEAD - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); -======= - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); ->>>>>>> creating permit tests + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('returns zero if < first checkpoint block', async function () { await time.advanceBlock(); -<<<<<<< HEAD - const t1 = await this.token.mint(holder, initalTokenId); -======= - const t1 = await this.token.mint(holder, supply); ->>>>>>> creating permit tests + const t1 = await this.token.mint(holder, NFT0); await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); -<<<<<<< HEAD - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { @@ -742,47 +539,20 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); const t5 = await this.token.mint(holder, NFT3); -======= - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.mint(holder, supply); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.burn(10); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.burn(10); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.mint(holder, 20); ->>>>>>> creating permit tests await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); -<<<<<<< HEAD - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); -======= - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('10000000000000000000000000'); ->>>>>>> creating permit tests + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); }); }); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js index 7a7b8cf5d83..f22c904c724 100644 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -50,7 +50,7 @@ contract('ERC721Permit', function (accounts) { ); }); - describe.only('permit', function () { + describe('permit', function () { const wallet = Wallet.generate(); const owner = wallet.getAddressString(); From e55511a51244ac7b498c2a22c54e0f01184a9585 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 18:20:14 -0400 Subject: [PATCH 198/300] Updating ERC721Vote tests --- .../token/ERC721/extensions/ERC721Votes.sol | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 6a7ef27dbec..38285ed9832 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -199,9 +199,8 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _afterTokenTransfer( address from, - address to, - uint256 tokenId - ) internal virtual override{ + address to + ) internal virtual { _moveVotingPower(delegates(from), delegates(to), 1); } @@ -287,4 +286,17 @@ abstract contract ERC721Votes is ERC721, EIP712 { function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } + + /** + * @dev Moves token from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 tokenId) external returns (bool){ + _transfer(_msgSender(), recipient, tokenId); + _afterTokenTransfer(_msgSender(), recipient); + return true; + } } From 9eaa01ddbe31cd1dcd7323124983f68c79488c74 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 16:30:31 -0400 Subject: [PATCH 199/300] Renaming supplied tokenId on tests --- .../ERC721/extensions/draft-ERC721Permit.sol | 87 ------------------- .../ERC721/extensions/draft-IERC721Permit.sol | 60 ------------- 2 files changed, 147 deletions(-) delete mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol delete mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol deleted file mode 100644 index c00d15367ab..00000000000 --- a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) - -pragma solidity ^0.8.0; - -import "./draft-IERC721Permit.sol"; -import "./ERC721Enumerable.sol"; -import "../../../utils/cryptography/draft-EIP712.sol"; -import "../../../utils/cryptography/ECDSA.sol"; -import "../../../utils/Counters.sol"; - -/** - * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - * - * _Available since v3.4._ - */ -abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { - using Counters for Counters.Counter; - - mapping(address => Counters.Counter) private _nonces; - - // solhint-disable-next-line var-name-mixedcase - bytes32 private immutable _PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - - /** - * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. - * - * It's a good idea to use the same `name` that is defined as the ERC721 token name. - */ - constructor(string memory name) EIP712(name, "1") {} - - /** - * @dev See {IERC721Permit-permit}. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual override { - require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); - - bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); - - bytes32 hash = _hashTypedDataV4(structHash); - - address signer = ECDSA.recover(hash, v, r, s); - require(signer == owner, "ERC721Permit: invalid signature"); - - _approve(spender, value); - } - - /** - * @dev See {IERC721Permit-nonces}. - */ - function nonces(address owner) public view virtual override returns (uint256) { - return _nonces[owner].current(); - } - - /** - * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view override returns (bytes32) { - return _domainSeparatorV4(); - } - - /** - * @dev "Consume a nonce": return the current value and increment. - * - * _Available since v4.1._ - */ - function _useNonce(address owner) internal virtual returns (uint256 current) { - Counters.Counter storage nonce = _nonces[owner]; - current = nonce.current(); - nonce.increment(); - } -} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol deleted file mode 100644 index 61882f2de0d..00000000000 --- a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - */ -interface IERC721Permit { - /** - * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, - * given ``owner``'s signed approval. - * - * IMPORTANT: The same issues {IERC721-approve} has related to transaction - * ordering also apply here. - * - * Emits an {Approval} event. - * - * Requirements: - * - * - `spender` cannot be the zero address. - * - `deadline` must be a timestamp in the future. - * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` - * over the EIP712-formatted function arguments. - * - the signature must use ``owner``'s current nonce (see {nonces}). - * - * For more information on the signature format, see the - * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP - * section]. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - /** - * @dev Returns the current nonce for `owner`. This value must be - * included whenever a signature is generated for {permit}. - * - * Every successful call to {permit} increases ``owner``'s nonce by one. This - * prevents a signature from being used multiple times. - */ - function nonces(address owner) external view returns (uint256); - - /** - * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view returns (bytes32); -} From 29b0e160e6bc8a766b0e1de624653fea44367820 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 17:45:23 -0400 Subject: [PATCH 200/300] Updating tests based on new contract changes --- contracts/mocks/ERC721PermitMock.sol | 20 ------------------- contracts/mocks/ERC721VotesMock.sol | 4 ++++ .../token/ERC721/extensions/ERC721Votes.sol | 19 +++++++++++++++++- 3 files changed, 22 insertions(+), 21 deletions(-) delete mode 100644 contracts/mocks/ERC721PermitMock.sol diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol deleted file mode 100644 index 37a860ef4dc..00000000000 --- a/contracts/mocks/ERC721PermitMock.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../token/ERC721/extensions/draft-ERC721Permit.sol"; - -contract ERC721PermitMock is ERC721Permit { - constructor( - string memory name, - string memory symbol, - address initialAccount, - uint256 tokenId - ) payable ERC721(name, symbol) ERC721Permit(name) { - _mint(initialAccount, tokenId); - } - - function getChainId() external view returns (uint256) { - return block.chainid; - } -} diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index bde65e5a5ff..869dc27d6e3 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,7 +5,11 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { +<<<<<<< HEAD constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} +======= + constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {} +>>>>>>> Updating tests based on new contract changes function mint(address account, uint256 tokenId) public { _mint(account, tokenId); diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 38285ed9832..1e4e9a308d8 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -8,6 +8,7 @@ import "../../../utils/Counters.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; /** * @dev Extension of ERC721 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. @@ -43,7 +44,12 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. * * It's a good idea to use the same `name` that is defined as the ERC721 token name. +<<<<<<< HEAD */ +======= + + constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ +>>>>>>> Updating tests based on new contract changes /** * @dev Emitted when an account changes their delegate. @@ -179,7 +185,8 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _mint(address account, uint256 tokenId) internal virtual override { super._mint(account, tokenId); - require(totalSupply() <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + _totalSupply += 1; + require(_totalSupply <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } @@ -189,6 +196,10 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _burn(uint256 tokenId) internal virtual override { super._burn(tokenId); +<<<<<<< HEAD +======= + _totalSupply -= 1; +>>>>>>> Updating tests based on new contract changes _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); } @@ -199,8 +210,14 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ function _afterTokenTransfer( address from, +<<<<<<< HEAD address to ) internal virtual { +======= + address to, + uint256 tokenId + ) internal virtual override{ +>>>>>>> Updating tests based on new contract changes _moveVotingPower(delegates(from), delegates(to), 1); } From 3c49d6ce1dea82c97127f50b841e87207e9b3775 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 20:15:54 -0400 Subject: [PATCH 201/300] Updating execution order inside of mint --- contracts/token/ERC721/extensions/ERC721Votes.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 1e4e9a308d8..eaf65ef2839 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -184,10 +184,11 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been increased. */ function _mint(address account, uint256 tokenId) internal virtual override { + require(_totalSupply+1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + super._mint(account, tokenId); _totalSupply += 1; - require(_totalSupply <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } From d344647905cc5f8a2be86f546d3d9447b6a5734e Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 16:25:14 -0400 Subject: [PATCH 202/300] Adding Mocks for testing and integrating nft minting to current workflow test --- .../extensions/GovernorERC721.test.js | 55 +++++++++++++------ 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index ee8ad8399da..11a0f382b28 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -1,4 +1,5 @@ -const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); +const { BN } = require('bn.js'); const Enums = require('../../helpers/enums'); const { @@ -15,21 +16,35 @@ contract('GovernorERC721Mock', function (accounts) { const name = 'OZ-Governor'; const tokenName = 'MockNFToken'; const tokenSymbol = 'MTKN'; - const initalTokenId = web3.utils.toWei('100'); + const NFT0 = web3.utils.toWei('100'); const NFT1 = web3.utils.toWei('10'); const NFT2 = web3.utils.toWei('20'); const NFT3 = web3.utils.toWei('30'); + const NFT4 = web3.utils.toWei('40'); + + // Must be the same as in contract + const ProposalState = { + Pending: new BN('0'), + Active: new BN('1'), + Canceled: new BN('2'), + Defeated: new BN('3'), + Succeeded: new BN('4'), + Queued: new BN('5'), + Expired: new BN('6'), + Executed: new BN('7'), + }; beforeEach(async function () { this.owner = owner; this.token = await Token.new(tokenName, tokenSymbol); this.mock = await Governor.new(name, this.token.address); this.receiver = await CallReceiver.new(); - await this.token.mint(owner, initalTokenId); + await this.token.mint(owner, NFT0); await this.token.mint(owner, NFT1); await this.token.mint(owner, NFT2); await this.token.mint(owner, NFT3); - + await this.token.mint(owner, NFT4); + await this.token.delegate(voter1, { from: voter1 }); await this.token.delegate(voter2, { from: voter2 }); await this.token.delegate(voter3, { from: voter3 }); @@ -55,20 +70,20 @@ contract('GovernorERC721Mock', function (accounts) { ], tokenHolder: owner, voters: [ - { voter: voter1, nftWeight: initalTokenId, support: Enums.VoteType.For }, - { voter: voter2, nftWeight: NFT1, support: Enums.VoteType.For }, - { voter: voter3, nftWeight: NFT2, support: Enums.VoteType.Against }, - { voter: voter4, nftWeight: NFT3, support: Enums.VoteType.Abstain }, - ] - } + { voter: voter1, nfts: [NFT0], support: Enums.VoteType.For }, + { voter: voter2, nfts: [NFT1, NFT2], support: Enums.VoteType.For }, + { voter: voter3, nfts: [NFT3], support: Enums.VoteType.Against }, + { voter: voter4, nfts: [NFT4], support: Enums.VoteType.Abstain }, + ], + }; }); afterEach(async function () { - expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); - for(const vote of this.receipts.castVote.filter(Boolean)){ + for (const vote of this.receipts.castVote.filter(Boolean)) { const { voter } = vote.logs.find(Boolean).args; - + expect(await this.mock.hasVoted(this.id, voter)).to.be.equal(true); expectEvent( @@ -77,19 +92,27 @@ contract('GovernorERC721Mock', function (accounts) { this.settings.voters.find(({ address }) => address === voter), ); - expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); + if (voter === voter2) { + expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('2'); + } else { + expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); + } } await this.mock.proposalVotes(this.id).then(result => { for (const [key, value] of Object.entries(Enums.VoteType)) { expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( - Object.values(this.settings.voters).filter(({ support }) => support === value).length.toString() + Object.values(this.settings.voters).filter(({ support }) => support === value).reduce( + (acc, { nfts }) => acc.add(new BN(nfts.length)), + new BN('0'), + ), ); } }); + + expect(await this.mock.state(this.id)).to.be.bignumber.equal(ProposalState.Executed); }); runGovernorWorkflow(); - }); }); From d687e09bdc1b945a167c3dd3753d2a550e8979fa Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 18:57:08 -0400 Subject: [PATCH 203/300] Governance adocs update --- contracts/mocks/ERC721VotesMock.sol | 12 +++---- contracts/mocks/UserTOkenerc721Mock.sol | 32 +++++++++++++++++++ .../token/ERC721/extensions/ERC721Votes.sol | 4 +++ 3 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 869dc27d6e3..b47bfd8f24e 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -2,14 +2,10 @@ pragma solidity ^0.8.0; -import "../token/ERC721/extensions/ERC721Votes.sol"; +import "../token/ERC721/extensions/draft-ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { -<<<<<<< HEAD - constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} -======= - constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {} ->>>>>>> Updating tests based on new contract changes + constructor(string memory name, string memory symbol) ERC721Votes(name, symbol) {} function mint(address account, uint256 tokenId) public { _mint(account, tokenId); @@ -23,7 +19,7 @@ contract ERC721VotesMock is ERC721Votes { return block.chainid; } - function _maxSupply() internal pure override returns(uint224){ - return uint224(4); + function _maxSupply() internal pure override returns (uint224) { + return uint224(5); } } diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol new file mode 100644 index 00000000000..b3874e66da5 --- /dev/null +++ b/contracts/mocks/UserTOkenerc721Mock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; + +contract MyToken is ERC721, EIP712, ERC721Votes { + constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._burn(account, amount); + } +} \ No newline at end of file diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index eaf65ef2839..819cedba82d 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -44,12 +44,16 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. * * It's a good idea to use the same `name` that is defined as the ERC721 token name. +<<<<<<< HEAD <<<<<<< HEAD */ ======= constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ >>>>>>> Updating tests based on new contract changes +======= + */ +>>>>>>> Governance adocs update /** * @dev Emitted when an account changes their delegate. From 101a82cc1ddcbbacecff3b8e7d27c0d78a91c657 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 14:46:27 -0400 Subject: [PATCH 204/300] Updating contracts listing order --- docs/modules/ROOT/pages/governance.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index a262a75af38..6014f778dc1 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -14,14 +14,14 @@ This governance protocol is generally implemented in a special-purpose contract OpenZeppelin’s Governor system was designed with a concern for compatibility with existing systems that were based on Compound’s GovernorAlpha and GovernorBravo. Because of this, you will find that many modules are presented in two variants, one of which is built for compatibility with those systems. -=== ERC721Votes - -The ERC721 extension to keep track of votes and vote delegation is one such case. - === ERC20Votes & ERC20VotesComp The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. +=== ERC721Votes + +The ERC721 extension to keep track of votes and vote delegation is one such case. + === Governor & GovernorCompatibilityBravo An OpenZeppelin Governor contract is by default not interface-compatible with GovernorAlpha or Bravo, since some of the functions are different or missing, although it shares all of the same events. However, it’s possible to opt in to full compatibility by inheriting from the GovernorCompatibilityBravo module. The contract will be cheaper to deploy and use without this module. From 5201c96ca175bd4d09198b8b755c28312d5408d7 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 15:02:56 -0400 Subject: [PATCH 205/300] Following lint suggestions --- test/governance/GovernorWorkflow.behavior.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js index e8e2416b19e..ae178d7c9de 100644 --- a/test/governance/GovernorWorkflow.behavior.js +++ b/test/governance/GovernorWorkflow.behavior.js @@ -31,9 +31,10 @@ function runGovernorWorkflow () { for (const voter of this.settings.voters) { if (voter.weight) { await this.token.transfer(voter.voter, voter.weight, { from: this.settings.tokenHolder }); - }else if(voter.nftWeight){ - await this.token.transferFrom(this.settings.tokenHolder, voter.voter, voter.nftWeight, { from: this.settings.tokenHolder }); - } + } else if (voter.nftWeight) { + await this.token.transferFrom(this.settings.tokenHolder, voter.voter, voter.nftWeight, + { from: this.settings.tokenHolder }); + } } } From 4f9cc0a3bcb3eb1afb5a7f34a925101c248e113d Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 15:18:09 -0400 Subject: [PATCH 206/300] Delete of test contract --- contracts/mocks/UserTOkenerc721Mock.sol | 32 ------------------------- 1 file changed, 32 deletions(-) delete mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol deleted file mode 100644 index b3874e66da5..00000000000 --- a/contracts/mocks/UserTOkenerc721Mock.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "@openzeppelin/contracts/token/ERC721.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; - -contract MyToken is ERC721, EIP712, ERC721Votes { - constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._afterTokenTransfer(from, to, amount); - } - - function _mint(address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._mint(to, amount); - } - - function _burn(address account, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._burn(account, amount); - } -} \ No newline at end of file From 4175ade6a7c9a90b70061af6ace64d33ff215cd1 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 18:14:44 -0400 Subject: [PATCH 207/300] Updating comment and documentation --- contracts/governance/IGovernor.sol | 2 +- docs/modules/ROOT/pages/governance.adoc | 27 ++++++++++--------------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index 50d9d9952df..d5dc08cd661 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -193,7 +193,7 @@ abstract contract IGovernor is IERC165 { function castVote(uint256 proposalId, uint8 support) public virtual returns (uint256 balance); /** - * @dev Cast a with a reason + * @dev Cast a vote with a reason * * Emits a {VoteCast} event. */ diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 6014f778dc1..353b35f6a0e 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -129,7 +129,7 @@ If your project requires The voting power of each account in our governance setu // SPDX-License-Identifier: MIT pragma solidity ^0.8.2; -import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; contract MyToken is ERC721, EIP712, ERC721Votes { @@ -137,25 +137,20 @@ contract MyToken is ERC721, EIP712, ERC721Votes { // The functions below are overrides required by Solidity. - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._afterTokenTransfer(from, to, amount); + function _afterTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal override(ERC721, ERC721Votes) { + super._afterTokenTransfer(from, to, tokenId); } - function _mint(address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._mint(to, amount); + function _mint(address to, uint256 tokenId) internal override(ERC721, ERC721Votes) { + super._mint(to, tokenId); } - function _burn(address account, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._burn(account, amount); + function _burn(uint256 tokenId) internal override(ERC721, ERC721Votes) { + super._burn(tokenId); } } ``` From c8e1c944e80cbeccd9e7b077b8ff18eec1fec427 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 12:45:06 -0400 Subject: [PATCH 208/300] Update docs/modules/ROOT/pages/governance.adoc Co-authored-by: Francisco Giordano --- docs/modules/ROOT/pages/governance.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 353b35f6a0e..825bc5b2139 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -159,7 +159,7 @@ NOTE: Voting power could be determined in different ways: multiple ERC20 tokens, === Governor -Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, 3) what options people have when casting a vote and how those votes are counted, and 4)what type of token should be use to vote. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. +Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, 3) what options people have when casting a vote and how those votes are counted, and 4) what type of token should be used to vote. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. For 1) we will use the GovernorVotes module, which hooks to an ERC20Votes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. From de45b91f5f62808c9ba840b5b21198642cb3a9f0 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 12:53:42 -0400 Subject: [PATCH 209/300] Update contracts/token/ERC721/extensions/ERC721Votes.sol Co-authored-by: Francisco Giordano --- contracts/token/ERC721/extensions/ERC721Votes.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 819cedba82d..cd647d0b7a0 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -188,7 +188,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been increased. */ function _mint(address account, uint256 tokenId) internal virtual override { - require(_totalSupply+1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + require(_totalSupply + 1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); super._mint(account, tokenId); _totalSupply += 1; From 6af56a9cd4216ece92a673441d61d8c7aa1ea8e9 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 12:54:01 -0400 Subject: [PATCH 210/300] Update docs/modules/ROOT/pages/governance.adoc Co-authored-by: Francisco Giordano --- docs/modules/ROOT/pages/governance.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 825bc5b2139..e951cfccd32 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -167,7 +167,7 @@ For 2) we will use GovernorVotesQuorumFraction which works together with ERC20Vo For 3) we will use GovernorCountingSimple, a module that offers 3 options to voters: For, Against, and Abstain, and where only For and Abstain votes are counted towards quorum. -For 4) we will use the GovernorVotesERC721 module, which hooks to an ERC721Votes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. +For 4) we will use the GovernorVotesERC721 module, which hooks to an ERC721Votes instance to determine the voting power of an account based on the NFTs they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. Besides these modules, Governor itself has some parameters we must set. From 9079397fe5de671228a96725d042dd9e597bc663 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 13:00:30 -0400 Subject: [PATCH 211/300] Update erc721.adoc "Voting rights" are generally fungible --- docs/modules/ROOT/pages/erc721.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/erc721.adoc b/docs/modules/ROOT/pages/erc721.adoc index 14dbdc97606..8d28fad2e6e 100644 --- a/docs/modules/ROOT/pages/erc721.adoc +++ b/docs/modules/ROOT/pages/erc721.adoc @@ -1,6 +1,6 @@ = ERC721 -We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate*, *voting rights*, or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. +We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate* or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. ERC721 is a more complex standard than ERC20, with multiple optional extensions, and is split across a number of contracts. The OpenZeppelin Contracts provide flexibility regarding how these are combined, along with custom useful extensions. Check out the xref:api:token/ERC721.adoc[API Reference] to learn more about these. From 6eb60dad857c1fea0609f31c544ac97c0217ce98 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 24 Nov 2021 16:17:09 -0400 Subject: [PATCH 212/300] Delete unused test file --- .../extensions/draft-ERC721Permit.test.js | 117 ------------------ 1 file changed, 117 deletions(-) delete mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js deleted file mode 100644 index f22c904c724..00000000000 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable */ - -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); -const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; - -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; - -const ERC721PermitMock = artifacts.require('ERC721PermitMock'); - -const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); - -const Permit = [ - { name: 'owner', type: 'address' }, - { name: 'spender', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint256' }, -]; - -contract('ERC721Permit', function (accounts) { - const [ initialHolder, spender, recipient, other ] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - - const initialTokenId = new BN('100'); - - beforeEach(async function () { - this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); - - // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id - // from within the EVM as from the JSON RPC interface. - // See https://github.com/trufflesuite/ganache-core/issues/515 - this.chainId = await this.token.getChainId(); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect( - await this.token.DOMAIN_SEPARATOR(), - ).to.equal( - await domainSeparator(name, version, this.chainId, this.token.address), - ); - }); - - describe('permit', function () { - const wallet = Wallet.generate(); - - const owner = wallet.getAddressString(); - const value = initialTokenId; - const nonce = 0; - const maxDeadline = MAX_UINT256; - - const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ - primaryType: 'Permit', - types: { EIP712Domain, Permit }, - domain: { name, version, chainId, verifyingContract }, - message: { owner, spender, value, nonce, deadline }, - }); - - it('accepts owner signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); - expect(await this.token.getApproved(value)).to.be.equal(spender); - }); - - it('rejects reused signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects other signature', async function () { - const otherWallet = Wallet.generate(); - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects expired permit', async function () { - const deadline = (await time.latest()) - time.duration.weeks(1); - - const data = buildData(this.chainId, this.token.address, deadline); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, deadline, v, r, s), - 'ERC721Permit: expired deadline', - ); - }); - }); -}); From 29e7a028cc3491ef34a26d556bcc3f6487b4c216 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 25 Nov 2021 09:54:14 -0400 Subject: [PATCH 213/300] Create Checkpoints and Voting libraries --- contracts/utils/Checkpoints.sol | 67 +++++++++++++++++ contracts/utils/Voting.sol | 126 ++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 contracts/utils/Checkpoints.sol create mode 100644 contracts/utils/Voting.sol diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol new file mode 100644 index 00000000000..408f4fc0d35 --- /dev/null +++ b/contracts/utils/Checkpoints.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./math/Math.sol"; +import "./math/SafeCast.sol"; + +library Checkpoints { + struct Checkpoint { + uint32 index; + uint224 value; + } + + struct History { + Checkpoint[] _checkpoints; + } + + function length(History storage self) internal view returns (uint256) { + return self._checkpoints.length; + } + + function at(History storage self, uint256 pos) internal view returns (Checkpoint memory) { + return self._checkpoints[pos]; + } + + function latest(History storage self) internal view returns (uint256) { + uint256 pos = length(self); + return pos == 0 ? 0 : at(self, pos - 1).value; + } + + function past(History storage self, uint256 index) internal view returns (uint256) { + require(index < block.number, "block not yet mined"); + + uint256 high = length(self); + uint256 low = 0; + while (low < high) { + uint256 mid = Math.average(low, high); + if (at(self, mid).index > index) { + high = mid; + } else { + low = mid + 1; + } + } + return high == 0 ? 0 : at(self, high - 1).value; + } + + function push( + History storage self, + uint256 value + ) internal returns (uint256, uint256) { + uint256 pos = length(self); + uint256 old = latest(self); + if (pos > 0 && self._checkpoints[pos - 1].index == block.number) { + self._checkpoints[pos - 1].value = SafeCast.toUint224(value); + } else { + self._checkpoints.push(Checkpoint({ index: SafeCast.toUint32(block.number), value: SafeCast.toUint224(value) })); + } + return (old, value); + } + + function push( + History storage self, + function(uint256, uint256) view returns (uint256) op, + uint256 delta + ) internal returns (uint256, uint256) { + return push(self, op(latest(self), delta)); + } +} diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol new file mode 100644 index 00000000000..b834e239a8f --- /dev/null +++ b/contracts/utils/Voting.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./Checkpoints.sol"; + +library Voting { + using Checkpoints for Checkpoints.History; + + struct Votes { + mapping(address => address) _delegation; + mapping(address => Checkpoints.History) _userCheckpoints; + Checkpoints.History _totalCheckpoints; + } + + function getVotes(Votes storage self, address account) internal view returns (uint256) { + return self._userCheckpoints[account].latest(); + } + + function getVotesAt(Votes storage self, address account, uint256 timestamp) internal view returns (uint256) { + return self._userCheckpoints[account].past(timestamp); + } + + function getTotalVotes(Votes storage self) internal view returns (uint256) { + return self._totalCheckpoints.latest(); + } + + function getTotalVotesAt(Votes storage self, uint256 timestamp) internal view returns (uint256) { + return self._totalCheckpoints.past(timestamp); + } + + function delegates(Votes storage self, address account) internal view returns (address) { + return self._delegation[account]; + } + + function delegate(Votes storage self, address account, address newDelegation, uint256 balance) internal { + address oldDelegation = delegates(self, account); + self._delegation[account] = newDelegation; + moveVotingPower(self, oldDelegation, newDelegation, balance, _dummy); + } + + function delegate( + Votes storage self, + address account, + address newDelegation, + uint256 balance, + function(address, uint256, uint256) hookDelegateVotesChanged + ) internal { + address oldDelegation = delegates(self, account); + self._delegation[account] = newDelegation; + moveVotingPower(self, oldDelegation, newDelegation, balance, hookDelegateVotesChanged); + } + + function mint(Votes storage self, address to, uint256 amount) internal { + self._totalCheckpoints.push(_add, amount); + moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); + } + + function mint( + Votes storage self, + address to, + uint256 amount, + function(address, uint256, uint256) hookDelegateVotesChanged + ) internal { + self._totalCheckpoints.push(_add, amount); + moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); + } + + function burn(Votes storage self, address from, uint256 amount) internal { + self._totalCheckpoints.push(_subtract, amount); + moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); + } + + function burn( + Votes storage self, + address from, + uint256 amount, + function(address, uint256, uint256) hookDelegateVotesChanged + ) internal { + self._totalCheckpoints.push(_subtract, amount); + moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); + } + + function transfer(Votes storage self, address from, address to, uint256 amount) internal { + moveVotingPower(self, delegates(self, from), delegates(self, to), amount, _dummy); + } + + function transfer( + Votes storage self, + address from, + address to, + uint256 amount, + function(address, uint256, uint256) hookDelegateVotesChanged + ) internal { + moveVotingPower(self, delegates(self, from), delegates(self, to), amount, hookDelegateVotesChanged); + } + + function _moveVotingPower( + Votes storage self, + address src, + address dst, + uint256 amount, + function(address, uint256, uint256) hookDelegateVotesChanged + ) private { + if (src != dst && amount > 0) { + if (src != address(0)) { + (uint256 oldValue, uint256 newValue) = self._userCheckpoints[src].push(_subtract, amount); + hookDelegateVotesChanged(src, oldValue, newValue); + } + if (dst != address(0)) { + (uint256 oldValue, uint256 newValue) = self._userCheckpoints[dst].push(_add, amount); + hookDelegateVotesChanged(dst, oldValue, newValue); + } + } + } + + function _add(uint256 a, uint256 b) private pure returns (uint256) { + return a + b; + } + + function _subtract(uint256 a, uint256 b) private pure returns (uint256) { + return a - b; + } + + function _dummy(address, uint256, uint256) private pure {} + +} From 9a26c4d9dfb8ca5931493ef2f1b20795f2863873 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 09:22:58 -0400 Subject: [PATCH 214/300] Add function to Voting library --- contracts/utils/Voting.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol index b834e239a8f..3a60a7f145b 100644 --- a/contracts/utils/Voting.sol +++ b/contracts/utils/Voting.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import "./Checkpoints.sol"; +import "hardhat/console.sol"; library Voting { using Checkpoints for Checkpoints.History; @@ -23,6 +24,10 @@ library Voting { function getTotalVotes(Votes storage self) internal view returns (uint256) { return self._totalCheckpoints.latest(); } + + function getTotalAccountVotes(Votes storage self, address account) internal view returns (uint256) { + return self._userCheckpoints[account].length(); + } function getTotalVotesAt(Votes storage self, uint256 timestamp) internal view returns (uint256) { return self._totalCheckpoints.past(timestamp); @@ -51,6 +56,7 @@ library Voting { } function mint(Votes storage self, address to, uint256 amount) internal { + console.log("minting 1"); self._totalCheckpoints.push(_add, amount); moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); } @@ -61,11 +67,13 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) internal { + console.log("minting 2"); self._totalCheckpoints.push(_add, amount); moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); } function burn(Votes storage self, address from, uint256 amount) internal { + console.log("burn 1"); self._totalCheckpoints.push(_subtract, amount); moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); } @@ -76,6 +84,7 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) internal { + console.log("burn 2"); self._totalCheckpoints.push(_subtract, amount); moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); } @@ -101,6 +110,8 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) private { + console.log("moving voting power"); + console.log(block.number); if (src != dst && amount > 0) { if (src != address(0)) { (uint256 oldValue, uint256 newValue) = self._userCheckpoints[src].push(_subtract, amount); From af23b86b27d8bdadccdb2f84345692a39189b0c3 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 11:30:11 -0400 Subject: [PATCH 215/300] Documentation for Checkpoints library --- contracts/utils/Checkpoints.sol | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index 408f4fc0d35..3e34f1f6933 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -4,6 +4,9 @@ pragma solidity ^0.8.0; import "./math/Math.sol"; import "./math/SafeCast.sol"; +/** + * @dev Checkpoints operations. + */ library Checkpoints { struct Checkpoint { uint32 index; @@ -14,19 +17,31 @@ library Checkpoints { Checkpoint[] _checkpoints; } + /** + * @dev Returns checkpoints length. + */ function length(History storage self) internal view returns (uint256) { return self._checkpoints.length; } + /** + * @dev Returns checkpoints at given position. + */ function at(History storage self, uint256 pos) internal view returns (Checkpoint memory) { return self._checkpoints[pos]; } + /** + * @dev Returns total amount of checkpoints. + */ function latest(History storage self) internal view returns (uint256) { uint256 pos = length(self); return pos == 0 ? 0 : at(self, pos - 1).value; } + /** + * @dev Returns checkpoints at given block number. + */ function past(History storage self, uint256 index) internal view returns (uint256) { require(index < block.number, "block not yet mined"); @@ -43,6 +58,9 @@ library Checkpoints { return high == 0 ? 0 : at(self, high - 1).value; } + /** + * @dev Creates checkpoint + */ function push( History storage self, uint256 value @@ -57,6 +75,9 @@ library Checkpoints { return (old, value); } + /** + * @dev Creates checkpoint + */ function push( History storage self, function(uint256, uint256) view returns (uint256) op, From 9b4d4cf469a8415ab32958c81fa6ca7d2c2a9fca Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 12:06:34 -0400 Subject: [PATCH 216/300] Add Mock contracts for Voting and checkpoints test --- contracts/mocks/CheckpointsImpl.sol | 31 ++++++++++++++++ contracts/mocks/VotingImpl.sol | 56 +++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 contracts/mocks/CheckpointsImpl.sol create mode 100644 contracts/mocks/VotingImpl.sol diff --git a/contracts/mocks/CheckpointsImpl.sol b/contracts/mocks/CheckpointsImpl.sol new file mode 100644 index 00000000000..e5e0f92cd6c --- /dev/null +++ b/contracts/mocks/CheckpointsImpl.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/Checkpoints.sol"; + +contract CheckpointsImpl { + using Checkpoints for Checkpoints.History; + + Checkpoints.History private _totalCheckpoints; + + function length() public view returns (uint256) { + return _totalCheckpoints.length(); + } + + function at(uint256 pos) public view returns (Checkpoints.Checkpoint memory) { + return _totalCheckpoints.at(pos); + } + + function latest() public view returns (uint256) { + return _totalCheckpoints.latest(); + } + + function past(uint256 index) public view returns (uint256) { + return _totalCheckpoints.past(index); + } + + function push(uint256 value) public returns (uint256, uint256) { + return _totalCheckpoints.push(value); + } +} diff --git a/contracts/mocks/VotingImpl.sol b/contracts/mocks/VotingImpl.sol new file mode 100644 index 00000000000..6dd7990b97c --- /dev/null +++ b/contracts/mocks/VotingImpl.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/Voting.sol"; + +contract VotingImpl { + using Voting for Voting.Votes; + + Voting.Votes private _votes; + + function getVotes(address account) public view returns (uint256) { + return _votes.getVotes(account); + } + + function getVotesAt(address account, uint256 timestamp) public view returns (uint256) { + return _votes.getVotesAt(account, timestamp); + } + + function getTotalAccountVotesAt(address account, uint32 pos) public view returns (Checkpoints.Checkpoint memory) { + return _votes.getTotalAccountVotesAt(account, pos); + } + + function getTotalVotes() public view returns (uint256) { + return _votes.getTotalVotes(); + } + + function getTotalAccountVotes(address account) public view returns (uint256) { + return _votes.getTotalAccountVotes(account); + } + + function getTotalVotesAt(uint256 timestamp) public view returns (uint256) { + return _votes.getTotalVotesAt(timestamp); + } + + function delegates(address account) public view returns (address) { + return _votes.delegates(account); + } + + function delegate(address account, address newDelegation, uint256 balance) public { + return _votes.delegate(account, newDelegation, balance); + } + + function mint(address to, uint256 amount) public { + return _votes.mint(to, amount); + } + + function burn(address from, uint256 amount) public { + return _votes.burn(from, amount); + } + + function transfer(address from, address to, uint256 amount) public { + return _votes.transfer(from, to, amount); + } + +} From aa3c0db7b1be2490e70e514ad7f548c18d54b4cc Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 18:57:08 -0400 Subject: [PATCH 217/300] Governance adocs update --- contracts/mocks/ERC721VotesMock.sol | 4 +++ contracts/mocks/UserTOkenerc721Mock.sol | 32 +++++++++++++++++++ .../token/ERC721/extensions/ERC721Votes.sol | 4 +++ docs/modules/ROOT/pages/erc721.adoc | 2 +- docs/modules/ROOT/pages/governance.adoc | 4 +++ 5 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index b47bfd8f24e..13dd53a85ae 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,7 +5,11 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/draft-ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { +<<<<<<< HEAD constructor(string memory name, string memory symbol) ERC721Votes(name, symbol) {} +======= + constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} +>>>>>>> Governance adocs update function mint(address account, uint256 tokenId) public { _mint(account, tokenId); diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol new file mode 100644 index 00000000000..b3874e66da5 --- /dev/null +++ b/contracts/mocks/UserTOkenerc721Mock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; + +contract MyToken is ERC721, EIP712, ERC721Votes { + constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._burn(account, amount); + } +} \ No newline at end of file diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index cd647d0b7a0..3756d1e2e82 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -45,12 +45,16 @@ abstract contract ERC721Votes is ERC721, EIP712 { * * It's a good idea to use the same `name` that is defined as the ERC721 token name. <<<<<<< HEAD +<<<<<<< HEAD <<<<<<< HEAD */ ======= constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ >>>>>>> Updating tests based on new contract changes +======= + */ +>>>>>>> Governance adocs update ======= */ >>>>>>> Governance adocs update diff --git a/docs/modules/ROOT/pages/erc721.adoc b/docs/modules/ROOT/pages/erc721.adoc index 8d28fad2e6e..14dbdc97606 100644 --- a/docs/modules/ROOT/pages/erc721.adoc +++ b/docs/modules/ROOT/pages/erc721.adoc @@ -1,6 +1,6 @@ = ERC721 -We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate* or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. +We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate*, *voting rights*, or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. ERC721 is a more complex standard than ERC20, with multiple optional extensions, and is split across a number of contracts. The OpenZeppelin Contracts provide flexibility regarding how these are combined, along with custom useful extensions. Check out the xref:api:token/ERC721.adoc[API Reference] to learn more about these. diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index e951cfccd32..28c607e703a 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -14,6 +14,10 @@ This governance protocol is generally implemented in a special-purpose contract OpenZeppelin’s Governor system was designed with a concern for compatibility with existing systems that were based on Compound’s GovernorAlpha and GovernorBravo. Because of this, you will find that many modules are presented in two variants, one of which is built for compatibility with those systems. +=== ERC721Votes + +The ERC721 extension to keep track of votes and vote delegation is one such case. + === ERC20Votes & ERC20VotesComp The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. From 2a3e09098f34813e915beee5f664343d5fa0b40c Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 14:13:44 -0400 Subject: [PATCH 218/300] Removing test contract --- contracts/mocks/UserTOkenerc721Mock.sol | 32 ------------------------- 1 file changed, 32 deletions(-) delete mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol deleted file mode 100644 index b3874e66da5..00000000000 --- a/contracts/mocks/UserTOkenerc721Mock.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "@openzeppelin/contracts/token/ERC721.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; - -contract MyToken is ERC721, EIP712, ERC721Votes { - constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._afterTokenTransfer(from, to, amount); - } - - function _mint(address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._mint(to, amount); - } - - function _burn(address account, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._burn(account, amount); - } -} \ No newline at end of file From e3730e135573fe894be7a19162519e0cd122de01 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sat, 30 Oct 2021 20:49:29 -0400 Subject: [PATCH 219/300] Initial contracts creation --- .../ERC721/extensions/draft-ERC721Permit.sol | 87 +++++++++++++++++++ .../ERC721/extensions/draft-IERC721Permit.sol | 60 +++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol create mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol new file mode 100644 index 00000000000..c00d15367ab --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) + +pragma solidity ^0.8.0; + +import "./draft-IERC721Permit.sol"; +import "./ERC721Enumerable.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; +import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/Counters.sol"; + +/** + * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * _Available since v3.4._ + */ +abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { + using Counters for Counters.Counter; + + mapping(address => Counters.Counter) private _nonces; + + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC721 token name. + */ + constructor(string memory name) EIP712(name, "1") {} + + /** + * @dev See {IERC721Permit-permit}. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); + + bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + + bytes32 hash = _hashTypedDataV4(structHash); + + address signer = ECDSA.recover(hash, v, r, s); + require(signer == owner, "ERC721Permit: invalid signature"); + + _approve(spender, value); + } + + /** + * @dev See {IERC721Permit-nonces}. + */ + function nonces(address owner) public view virtual override returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } +} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol new file mode 100644 index 00000000000..61882f2de0d --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by + * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + */ +interface IERC721Permit { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC721-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} From 731586ffa174c7f4b0cb7cb7bc2b016f07c67527 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 16:30:31 -0400 Subject: [PATCH 220/300] Renaming supplied tokenId on tests --- .../ERC721/extensions/draft-ERC721Permit.sol | 87 ------------------- .../ERC721/extensions/draft-IERC721Permit.sol | 60 ------------- 2 files changed, 147 deletions(-) delete mode 100644 contracts/token/ERC721/extensions/draft-ERC721Permit.sol delete mode 100644 contracts/token/ERC721/extensions/draft-IERC721Permit.sol diff --git a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol b/contracts/token/ERC721/extensions/draft-ERC721Permit.sol deleted file mode 100644 index c00d15367ab..00000000000 --- a/contracts/token/ERC721/extensions/draft-ERC721Permit.sol +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Permit.sol) - -pragma solidity ^0.8.0; - -import "./draft-IERC721Permit.sol"; -import "./ERC721Enumerable.sol"; -import "../../../utils/cryptography/draft-EIP712.sol"; -import "../../../utils/cryptography/ECDSA.sol"; -import "../../../utils/Counters.sol"; - -/** - * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - * - * _Available since v3.4._ - */ -abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit, EIP712 { - using Counters for Counters.Counter; - - mapping(address => Counters.Counter) private _nonces; - - // solhint-disable-next-line var-name-mixedcase - bytes32 private immutable _PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - - /** - * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. - * - * It's a good idea to use the same `name` that is defined as the ERC721 token name. - */ - constructor(string memory name) EIP712(name, "1") {} - - /** - * @dev See {IERC721Permit-permit}. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual override { - require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); - - bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); - - bytes32 hash = _hashTypedDataV4(structHash); - - address signer = ECDSA.recover(hash, v, r, s); - require(signer == owner, "ERC721Permit: invalid signature"); - - _approve(spender, value); - } - - /** - * @dev See {IERC721Permit-nonces}. - */ - function nonces(address owner) public view virtual override returns (uint256) { - return _nonces[owner].current(); - } - - /** - * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view override returns (bytes32) { - return _domainSeparatorV4(); - } - - /** - * @dev "Consume a nonce": return the current value and increment. - * - * _Available since v4.1._ - */ - function _useNonce(address owner) internal virtual returns (uint256 current) { - Counters.Counter storage nonce = _nonces[owner]; - current = nonce.current(); - nonce.increment(); - } -} diff --git a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol b/contracts/token/ERC721/extensions/draft-IERC721Permit.sol deleted file mode 100644 index 61882f2de0d..00000000000 --- a/contracts/token/ERC721/extensions/draft-IERC721Permit.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-IERC721Permit.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. - * - * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by - * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't - * need to send a transaction, and thus is not required to hold Ether at all. - */ -interface IERC721Permit { - /** - * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, - * given ``owner``'s signed approval. - * - * IMPORTANT: The same issues {IERC721-approve} has related to transaction - * ordering also apply here. - * - * Emits an {Approval} event. - * - * Requirements: - * - * - `spender` cannot be the zero address. - * - `deadline` must be a timestamp in the future. - * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` - * over the EIP712-formatted function arguments. - * - the signature must use ``owner``'s current nonce (see {nonces}). - * - * For more information on the signature format, see the - * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP - * section]. - */ - function permit( - address owner, - address spender, - uint256 value, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - /** - * @dev Returns the current nonce for `owner`. This value must be - * included whenever a signature is generated for {permit}. - * - * Every successful call to {permit} increases ``owner``'s nonce by one. This - * prevents a signature from being used multiple times. - */ - function nonces(address owner) external view returns (uint256); - - /** - * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view returns (bytes32); -} From 9dad23547bfc15f413fe15f2f32dec355e4016be Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 14:46:27 -0400 Subject: [PATCH 221/300] Updating contracts listing order --- docs/modules/ROOT/pages/governance.adoc | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 28c607e703a..e951cfccd32 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -14,10 +14,6 @@ This governance protocol is generally implemented in a special-purpose contract OpenZeppelin’s Governor system was designed with a concern for compatibility with existing systems that were based on Compound’s GovernorAlpha and GovernorBravo. Because of this, you will find that many modules are presented in two variants, one of which is built for compatibility with those systems. -=== ERC721Votes - -The ERC721 extension to keep track of votes and vote delegation is one such case. - === ERC20Votes & ERC20VotesComp The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. From ae4d23592b41be1bcb9f56e67c6ec71adaf271ae Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 10:26:22 -0400 Subject: [PATCH 222/300] Fixing checkpoints count --- contracts/token/ERC20/extensions/ERC20Votes.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index f4fd21d3e5b..dcbaf972220 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -7,7 +7,6 @@ import "./draft-ERC20Permit.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.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. @@ -174,7 +173,7 @@ abstract contract ERC20Votes is ERC20Permit { super._mint(account, amount); require(totalSupply() <= _maxSupply(), "ERC20Votes: total supply risks overflowing votes"); - _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); + _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); } /** From 897938f63d7168fab84781f1ec19a4757d4ad31f Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 1 Nov 2021 16:24:20 -0400 Subject: [PATCH 223/300] Adding _afterTokenTransfer to base ERC271 contract --- contracts/governance/extensions/GovernorVotesERC721.sol | 2 +- contracts/token/ERC20/extensions/ERC20Votes.sol | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index c2472a2e4e2..2e3079fc243 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC72GovernorVotesERC721.sol) +// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC721GovernorVotesERC721.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index dcbaf972220..5e176973ee2 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.0 (token/ERC20/extensions/ERC20Votes.sol) +// OpenZeppelin Contracts v4.3.2 (token/ERC20/extensions/ERC20Votes.sol) pragma solidity ^0.8.0; @@ -7,6 +7,7 @@ import "./draft-ERC20Permit.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.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. @@ -173,7 +174,7 @@ abstract contract ERC20Votes is ERC20Permit { super._mint(account, amount); require(totalSupply() <= _maxSupply(), "ERC20Votes: total supply risks overflowing votes"); - _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); + _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); } /** From c0cab85267fe555b546a3531fba3cdd1b059bd88 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 18:57:08 -0400 Subject: [PATCH 224/300] Governance adocs update --- contracts/mocks/UserTOkenerc721Mock.sol | 32 +++++++++++++++++++++++++ docs/modules/ROOT/pages/governance.adoc | 4 ++++ 2 files changed, 36 insertions(+) create mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol new file mode 100644 index 00000000000..b3874e66da5 --- /dev/null +++ b/contracts/mocks/UserTOkenerc721Mock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/token/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; + +contract MyToken is ERC721, EIP712, ERC721Votes { + constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC721, ERC721Votes) + { + super._burn(account, amount); + } +} \ No newline at end of file diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index e951cfccd32..28c607e703a 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -14,6 +14,10 @@ This governance protocol is generally implemented in a special-purpose contract OpenZeppelin’s Governor system was designed with a concern for compatibility with existing systems that were based on Compound’s GovernorAlpha and GovernorBravo. Because of this, you will find that many modules are presented in two variants, one of which is built for compatibility with those systems. +=== ERC721Votes + +The ERC721 extension to keep track of votes and vote delegation is one such case. + === ERC20Votes & ERC20VotesComp The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. From 3b031da98b1f6636e00f5190388eab352696fe62 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 14:13:44 -0400 Subject: [PATCH 225/300] Removing test contract --- contracts/mocks/UserTOkenerc721Mock.sol | 32 ------------------------- 1 file changed, 32 deletions(-) delete mode 100644 contracts/mocks/UserTOkenerc721Mock.sol diff --git a/contracts/mocks/UserTOkenerc721Mock.sol b/contracts/mocks/UserTOkenerc721Mock.sol deleted file mode 100644 index b3874e66da5..00000000000 --- a/contracts/mocks/UserTOkenerc721Mock.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "@openzeppelin/contracts/token/ERC721.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; - -contract MyToken is ERC721, EIP712, ERC721Votes { - constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._afterTokenTransfer(from, to, amount); - } - - function _mint(address to, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._mint(to, amount); - } - - function _burn(address account, uint256 amount) - internal - override(ERC721, ERC721Votes) - { - super._burn(account, amount); - } -} \ No newline at end of file From 9829c08400a250f465833d30a32160bd191096ba Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Sun, 31 Oct 2021 09:28:35 -0400 Subject: [PATCH 226/300] creating permit tests --- contracts/mocks/ERC721PermitMock.sol | 20 +++ .../extensions/draft-ERC721Permit.test.js | 117 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 contracts/mocks/ERC721PermitMock.sol create mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol new file mode 100644 index 00000000000..37a860ef4dc --- /dev/null +++ b/contracts/mocks/ERC721PermitMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../token/ERC721/extensions/draft-ERC721Permit.sol"; + +contract ERC721PermitMock is ERC721Permit { + constructor( + string memory name, + string memory symbol, + address initialAccount, + uint256 tokenId + ) payable ERC721(name, symbol) ERC721Permit(name) { + _mint(initialAccount, tokenId); + } + + function getChainId() external view returns (uint256) { + return block.chainid; + } +} diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js new file mode 100644 index 00000000000..7a7b8cf5d83 --- /dev/null +++ b/test/token/ERC721/extensions/draft-ERC721Permit.test.js @@ -0,0 +1,117 @@ +/* eslint-disable */ + +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const ERC721PermitMock = artifacts.require('ERC721PermitMock'); + +const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); + +const Permit = [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, +]; + +contract('ERC721Permit', function (accounts) { + const [ initialHolder, spender, recipient, other ] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; + + const initialTokenId = new BN('100'); + + beforeEach(async function () { + this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); + + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = await this.token.getChainId(); + }); + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect( + await this.token.DOMAIN_SEPARATOR(), + ).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address), + ); + }); + + describe.only('permit', function () { + const wallet = Wallet.generate(); + + const owner = wallet.getAddressString(); + const value = initialTokenId; + const nonce = 0; + const maxDeadline = MAX_UINT256; + + const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ + primaryType: 'Permit', + types: { EIP712Domain, Permit }, + domain: { name, version, chainId, verifyingContract }, + message: { owner, spender, value, nonce, deadline }, + }); + + it('accepts owner signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); + expect(await this.token.getApproved(value)).to.be.equal(spender); + }); + + it('rejects reused signature', async function () { + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects other signature', async function () { + const otherWallet = Wallet.generate(); + const data = buildData(this.chainId, this.token.address); + const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC721Permit: invalid signature', + ); + }); + + it('rejects expired permit', async function () { + const deadline = (await time.latest()) - time.duration.weeks(1); + + const data = buildData(this.chainId, this.token.address, deadline); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + const { v, r, s } = fromRpcSig(signature); + + await expectRevert( + this.token.permit(owner, spender, value, deadline, v, r, s), + 'ERC721Permit: expired deadline', + ); + }); + }); +}); From ada5c98d7db60180522a6f481e0db83ae7ac0c09 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 16:25:14 -0400 Subject: [PATCH 227/300] Adding Mocks for testing and integrating nft minting to current workflow test --- test/governance/GovernorWorkflow.behavior.js | 8 +- .../extensions/GovernorERC721.test.js | 632 +++++++++++++++--- 2 files changed, 541 insertions(+), 99 deletions(-) diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js index ae178d7c9de..1fb4823389a 100644 --- a/test/governance/GovernorWorkflow.behavior.js +++ b/test/governance/GovernorWorkflow.behavior.js @@ -31,9 +31,11 @@ function runGovernorWorkflow () { for (const voter of this.settings.voters) { if (voter.weight) { await this.token.transfer(voter.voter, voter.weight, { from: this.settings.tokenHolder }); - } else if (voter.nftWeight) { - await this.token.transferFrom(this.settings.tokenHolder, voter.voter, voter.nftWeight, - { from: this.settings.tokenHolder }); + } else if (voter.nfts) { + for (const nft of voter.nfts) { + await this.token.transferFrom(this.settings.tokenHolder, voter.voter, nft, + { from: this.settings.tokenHolder }); + } } } } diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 11a0f382b28..0ef80342a7d 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -1,118 +1,558 @@ -const { expectEvent } = require('@openzeppelin/test-helpers'); -const { BN } = require('bn.js'); -const Enums = require('../../helpers/enums'); - -const { - runGovernorWorkflow, -} = require('./../GovernorWorkflow.behavior'); - -const Token = artifacts.require('ERC721VotesMock'); -const Governor = artifacts.require('GovernorERC721Mock'); -const CallReceiver = artifacts.require('CallReceiverMock'); - -contract('GovernorERC721Mock', function (accounts) { - const [ owner, voter1, voter2, voter3, voter4 ] = accounts; - - const name = 'OZ-Governor'; - const tokenName = 'MockNFToken'; - const tokenSymbol = 'MTKN'; - const NFT0 = web3.utils.toWei('100'); - const NFT1 = web3.utils.toWei('10'); - const NFT2 = web3.utils.toWei('20'); - const NFT3 = web3.utils.toWei('30'); - const NFT4 = web3.utils.toWei('40'); - - // Must be the same as in contract - const ProposalState = { - Pending: new BN('0'), - Active: new BN('1'), - Canceled: new BN('2'), - Defeated: new BN('3'), - Succeeded: new BN('4'), - Queued: new BN('5'), - Expired: new BN('6'), - Executed: new BN('7'), - }; +/* eslint-disable */ + +const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const { promisify } = require('util'); +const queue = promisify(setImmediate); + +const ERC721VotesMock = artifacts.require('ERC721VotesMock'); + +const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); + +const Delegation = [ + { name: 'delegatee', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'expiry', type: 'uint256' }, +]; + +async function countPendingTransactions() { + return parseInt( + await network.provider.send('eth_getBlockTransactionCountByNumber', ['pending']) + ); +} + +async function batchInBlock (txs) { + try { + // disable auto-mining + await network.provider.send('evm_setAutomine', [false]); + // send all transactions + const promises = txs.map(fn => fn()); + // wait for node to have all pending transactions + while (txs.length > await countPendingTransactions()) { + await queue(); + } + // mine one block + await network.provider.send('evm_mine'); + // fetch receipts + const receipts = await Promise.all(promises); + // Sanity check, all tx should be in the same block + const minedBlocks = new Set(receipts.map(({ receipt }) => receipt.blockNumber)); + expect(minedBlocks.size).to.equal(1); + + return receipts; + } finally { + // enable auto-mining + await network.provider.send('evm_setAutomine', [true]); + } +} + +contract('ERC721Votes', function (accounts) { + const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; + const NFT0 = new BN('10000000000000000000000000'); + const NFT1 = new BN('10'); + const NFT2 = new BN('20'); + const NFT3 = new BN('30'); + const NFT4 = new BN('40'); + const name = 'My Token'; + const symbol = 'MTKN'; + const version = '1'; beforeEach(async function () { - this.owner = owner; - this.token = await Token.new(tokenName, tokenSymbol); - this.mock = await Governor.new(name, this.token.address); - this.receiver = await CallReceiver.new(); - await this.token.mint(owner, NFT0); - await this.token.mint(owner, NFT1); - await this.token.mint(owner, NFT2); - await this.token.mint(owner, NFT3); - await this.token.mint(owner, NFT4); - - await this.token.delegate(voter1, { from: voter1 }); - await this.token.delegate(voter2, { from: voter2 }); - await this.token.delegate(voter3, { from: voter3 }); - await this.token.delegate(voter4, { from: voter4 }); + this.token = await ERC721VotesMock.new(name, symbol); + + // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id + // from within the EVM as from the JSON RPC interface. + // See https://github.com/trufflesuite/ganache-core/issues/515 + this.chainId = await this.token.getChainId(); + }); + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect( + await this.token.DOMAIN_SEPARATOR(), + ).to.equal( + await domainSeparator(name, version, this.chainId, this.token.address), + ); + }); + + it('minting restriction', async function () { + const lastTokenId = new BN('2').pow(new BN('224')); + this.token.mint(holder, NFT1); + this.token.mint(holder, NFT2); + this.token.mint(holder, NFT3); + this.token.mint(holder, NFT0); + this.token.mint(holder, NFT4); + + await expectRevert( + this.token.mint(holder, lastTokenId), + 'ERC721Votes: total supply risks overflowing votes', + ); }); - 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('4'); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16'); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + describe('set delegation', function () { + describe('call', function () { + it('delegation with tokens', async function () { + await this.token.mint(holder, NFT0); + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: '0', + newBalance: '1', + }); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + + expect(await this.token.getVotes(holder)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); + }); + + it('delegation without tokens', async function () { + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + }); + }); + + describe('with signature', function () { + const delegator = Wallet.generate(); + const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); + const nonce = 0; + + const buildData = (chainId, verifyingContract, message) => ({ data: { + primaryType: 'Delegation', + types: { EIP712Domain, Delegation }, + domain: { name, version, chainId, verifyingContract }, + message, + }}); + + beforeEach(async function () { + await this.token.mint(delegatorAddress, NFT0); + }); + + it('accept signed delegation', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + expectEvent(receipt, 'DelegateChanged', { + delegator: delegatorAddress, + fromDelegate: ZERO_ADDRESS, + toDelegate: delegatorAddress, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: delegatorAddress, + previousBalance: '0', + newBalance: '1', + }); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); + + expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1'); + }); + + it('rejects reused signature', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), + 'ERC721Votes: invalid nonce', + ); + }); + + it('rejects bad delegatee', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + const { logs } = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); + const { args } = logs.find(({ event }) => event == 'DelegateChanged'); + expect(args.delegator).to.not.be.equal(delegatorAddress); + expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); + expect(args.toDelegate).to.be.equal(holderDelegatee); + }); + + it('rejects bad nonce', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), + 'ERC721Votes: invalid nonce', + ); + }); + + it('rejects expired permit', async function () { + const expiry = (await time.latest()) - time.duration.weeks(1); + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.token.address, { + delegatee: delegatorAddress, + nonce, + expiry, + }), + )); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), + 'ERC721Votes: signature expired', + ); + }); + }); + }); + + describe('change delegation', function () { + beforeEach(async function () { + await this.token.mint(holder, NFT0); + await this.token.delegate(holder, { from: holder }); + }); + + it('call', async function () { + expect(await this.token.delegates(holder)).to.be.equal(holder); + + const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: holder, + toDelegate: holderDelegatee, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: '1', + newBalance: '0', + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holderDelegatee, + previousBalance: '0', + newBalance: '1', + }); + + expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); + + expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); + expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); + }); }); - describe('voting with ERC721 token', function () { + describe('transfers', function () { beforeEach(async function () { - this.settings = { - proposal: [ - [ this.receiver.address ], - [ web3.utils.toWei('0') ], - [ this.receiver.contract.methods.mockFunction().encodeABI() ], - '', - ], - tokenHolder: owner, - voters: [ - { voter: voter1, nfts: [NFT0], support: Enums.VoteType.For }, - { voter: voter2, nfts: [NFT1, NFT2], support: Enums.VoteType.For }, - { voter: voter3, nfts: [NFT3], support: Enums.VoteType.Against }, - { voter: voter4, nfts: [NFT4], support: Enums.VoteType.Abstain }, - ], - }; + await this.token.mint(holder, NFT0); + }); + + it('no delegation', async function () { + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); + 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.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); + + 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 = '0'; + }); + + it('receiver delegation', async function () { + await this.token.delegate(recipient, { from: recipient }); + + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); + 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.transferFrom(holder, recipient, NFT0, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); + 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'; }); afterEach(async function () { - expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); + expect(await this.token.getVotes(holder)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); + + // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" + const blockNumber = await time.latestBlock(); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getPastVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes); + }); + }); + + // 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, NFT0); + await this.token.mint(holder, NFT1); + await this.token.mint(holder, NFT2); + await this.token.mint(holder, NFT3); + }); + + describe('balanceOf', function () { + it('grants to initial account', async function () { + expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('4'); + }); + }); + + describe('numCheckpoints', function () { + it('returns the number of checkpoints for a delegate', async function () { - for (const vote of this.receipts.castVote.filter(Boolean)) { - const { voter } = vote.logs.find(Boolean).args; + await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability + await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const t1 = await this.token.delegate(other1, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + + const t3 = await this.token.transferFrom(recipient, other2, NFT2, { from: recipient }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - expect(await this.mock.hasVoted(this.id, voter)).to.be.equal(true); + const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - expectEvent( - vote, - 'VoteCast', - this.settings.voters.find(({ address }) => address === voter), + expect(await this.token.checkpointAt(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); + expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); + expect(await this.token.checkpointAt(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); + expect(await this.token.checkpointAt(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); + + await time.advanceBlock(); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('1'); + }); + + it('does not add more than one checkpoint in a block', async function () { + await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); + await this.token.transferFrom(holder, recipient, NFT2, { 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.transferFrom(recipient, other2, NFT1, { from: recipient, gas: 200000 }), + () => this.token.transferFrom(recipient, other2, NFT2, { from: recipient, gas: 200000 }), + ]); + + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + expect(await this.token.checkpointAt(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); + + const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); + }); + }); + + describe('getPastVotes', function () { + it('reverts if block number >= current block', async function () { + await expectRevert( + this.token.getPastVotes(other1, 5e10), + 'block not yet mined', ); + }); - if (voter === voter2) { - expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('2'); - } else { - expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); - } - } + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('4'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); + }); - await this.mock.proposalVotes(this.id).then(result => { - for (const [key, value] of Object.entries(Enums.VoteType)) { - expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( - Object.values(this.settings.voters).filter(({ support }) => support === value).reduce( - (acc, { nfts }) => acc.add(new BN(nfts.length)), - new BN('0'), - ), - ); - } + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); }); - expect(await this.mock.state(this.id)).to.be.bignumber.equal(ProposalState.Executed); + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const total = await this.token.balanceOf(holder); + + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.transferFrom(holder, other2, NFT1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.transferFrom(holder, other2, NFT2, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.transferFrom(other2, holder, NFT2, { from: other2 }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal(total); + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(total); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3'); + expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); + }); }); + }); - runGovernorWorkflow(); + describe('getPastVotingPower', function () { + beforeEach(async function () { + await this.token.delegate(holder, { from: holder }); + }); + + it('reverts if block number >= current block', async function () { + await expectRevert( + this.token.getPastVotingPower(5e10), + 'ERC721Votes: block not yet mined', + ); + }); + + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastVotingPower(0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + t1 = await this.token.mint(holder, NFT0); + + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const t1 = await this.token.mint(holder, NFT0); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.mint(holder, NFT1); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.burn(NFT1); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.mint(holder, NFT2); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.burn(NFT2); + await time.advanceBlock(); + await time.advanceBlock(); + const t5 = await this.token.mint(holder, NFT3); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotingPower(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastVotingPower(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + }); }); }); From c730f4db6fbe566b7850145f41031514eae6d7e9 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 2 Nov 2021 17:02:56 -0400 Subject: [PATCH 228/300] Implementing override test --- .../extensions/GovernorERC721.test.js | 632 +++--------------- 1 file changed, 96 insertions(+), 536 deletions(-) diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 0ef80342a7d..11a0f382b28 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -1,558 +1,118 @@ -/* eslint-disable */ - -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); -const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; - -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; - -const { promisify } = require('util'); -const queue = promisify(setImmediate); - -const ERC721VotesMock = artifacts.require('ERC721VotesMock'); - -const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); - -const Delegation = [ - { name: 'delegatee', type: 'address' }, - { name: 'nonce', type: 'uint256' }, - { name: 'expiry', type: 'uint256' }, -]; - -async function countPendingTransactions() { - return parseInt( - await network.provider.send('eth_getBlockTransactionCountByNumber', ['pending']) - ); -} - -async function batchInBlock (txs) { - try { - // disable auto-mining - await network.provider.send('evm_setAutomine', [false]); - // send all transactions - const promises = txs.map(fn => fn()); - // wait for node to have all pending transactions - while (txs.length > await countPendingTransactions()) { - await queue(); - } - // mine one block - await network.provider.send('evm_mine'); - // fetch receipts - const receipts = await Promise.all(promises); - // Sanity check, all tx should be in the same block - const minedBlocks = new Set(receipts.map(({ receipt }) => receipt.blockNumber)); - expect(minedBlocks.size).to.equal(1); - - return receipts; - } finally { - // enable auto-mining - await network.provider.send('evm_setAutomine', [true]); - } -} - -contract('ERC721Votes', function (accounts) { - const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; - const NFT0 = new BN('10000000000000000000000000'); - const NFT1 = new BN('10'); - const NFT2 = new BN('20'); - const NFT3 = new BN('30'); - const NFT4 = new BN('40'); - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; +const { expectEvent } = require('@openzeppelin/test-helpers'); +const { BN } = require('bn.js'); +const Enums = require('../../helpers/enums'); + +const { + runGovernorWorkflow, +} = require('./../GovernorWorkflow.behavior'); + +const Token = artifacts.require('ERC721VotesMock'); +const Governor = artifacts.require('GovernorERC721Mock'); +const CallReceiver = artifacts.require('CallReceiverMock'); + +contract('GovernorERC721Mock', function (accounts) { + const [ owner, voter1, voter2, voter3, voter4 ] = accounts; + + const name = 'OZ-Governor'; + const tokenName = 'MockNFToken'; + const tokenSymbol = 'MTKN'; + const NFT0 = web3.utils.toWei('100'); + const NFT1 = web3.utils.toWei('10'); + const NFT2 = web3.utils.toWei('20'); + const NFT3 = web3.utils.toWei('30'); + const NFT4 = web3.utils.toWei('40'); + + // Must be the same as in contract + const ProposalState = { + Pending: new BN('0'), + Active: new BN('1'), + Canceled: new BN('2'), + Defeated: new BN('3'), + Succeeded: new BN('4'), + Queued: new BN('5'), + Expired: new BN('6'), + Executed: new BN('7'), + }; beforeEach(async function () { - this.token = await ERC721VotesMock.new(name, symbol); - - // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id - // from within the EVM as from the JSON RPC interface. - // See https://github.com/trufflesuite/ganache-core/issues/515 - this.chainId = await this.token.getChainId(); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect( - await this.token.DOMAIN_SEPARATOR(), - ).to.equal( - await domainSeparator(name, version, this.chainId, this.token.address), - ); - }); - - it('minting restriction', async function () { - const lastTokenId = new BN('2').pow(new BN('224')); - this.token.mint(holder, NFT1); - this.token.mint(holder, NFT2); - this.token.mint(holder, NFT3); - this.token.mint(holder, NFT0); - this.token.mint(holder, NFT4); - - await expectRevert( - this.token.mint(holder, lastTokenId), - 'ERC721Votes: total supply risks overflowing votes', - ); + this.owner = owner; + this.token = await Token.new(tokenName, tokenSymbol); + this.mock = await Governor.new(name, this.token.address); + this.receiver = await CallReceiver.new(); + await this.token.mint(owner, NFT0); + await this.token.mint(owner, NFT1); + await this.token.mint(owner, NFT2); + await this.token.mint(owner, NFT3); + await this.token.mint(owner, NFT4); + + await this.token.delegate(voter1, { from: voter1 }); + await this.token.delegate(voter2, { from: voter2 }); + await this.token.delegate(voter3, { from: voter3 }); + await this.token.delegate(voter4, { from: voter4 }); }); - describe('set delegation', function () { - describe('call', function () { - it('delegation with tokens', async function () { - await this.token.mint(holder, NFT0); - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegate(holder, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: '0', - newBalance: '1', - }); - - expect(await this.token.delegates(holder)).to.be.equal(holder); - - expect(await this.token.getVotes(holder)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); - }); - - it('delegation without tokens', async function () { - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegate(holder, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); - - expect(await this.token.delegates(holder)).to.be.equal(holder); - }); - }); - - describe('with signature', function () { - const delegator = Wallet.generate(); - const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); - const nonce = 0; - - const buildData = (chainId, verifyingContract, message) => ({ data: { - primaryType: 'Delegation', - types: { EIP712Domain, Delegation }, - domain: { name, version, chainId, verifyingContract }, - message, - }}); - - beforeEach(async function () { - await this.token.mint(delegatorAddress, NFT0); - }); - - it('accept signed delegation', async function () { - const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - )); - - expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - expectEvent(receipt, 'DelegateChanged', { - delegator: delegatorAddress, - fromDelegate: ZERO_ADDRESS, - toDelegate: delegatorAddress, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: delegatorAddress, - previousBalance: '0', - newBalance: '1', - }); - - expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); - - expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1'); - }); - - it('rejects reused signature', async function () { - const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - )); - - await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), - 'ERC721Votes: invalid nonce', - ); - }); - - it('rejects bad delegatee', async function () { - const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - )); - - const { logs } = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); - const { args } = logs.find(({ event }) => event == 'DelegateChanged'); - expect(args.delegator).to.not.be.equal(delegatorAddress); - expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); - expect(args.toDelegate).to.be.equal(holderDelegatee); - }); - - it('rejects bad nonce', async function () { - const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - )); - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), - 'ERC721Votes: invalid nonce', - ); - }); - - it('rejects expired permit', async function () { - const expiry = (await time.latest()) - time.duration.weeks(1); - const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry, - }), - )); - - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), - 'ERC721Votes: signature expired', - ); - }); - }); - }); - - describe('change delegation', function () { - beforeEach(async function () { - await this.token.mint(holder, NFT0); - await this.token.delegate(holder, { from: holder }); - }); - - it('call', async function () { - expect(await this.token.delegates(holder)).to.be.equal(holder); - - const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: holder, - toDelegate: holderDelegatee, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: '1', - newBalance: '0', - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holderDelegatee, - previousBalance: '0', - newBalance: '1', - }); - - expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); - - expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); - expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); - }); + 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('4'); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal('16'); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); }); - describe('transfers', function () { + describe('voting with ERC721 token', function () { beforeEach(async function () { - await this.token.mint(holder, NFT0); - }); - - it('no delegation', async function () { - const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); - 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.transferFrom(holder, recipient, NFT0, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); - - 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 = '0'; - }); - - it('receiver delegation', async function () { - await this.token.delegate(recipient, { from: recipient }); - - const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); - 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.transferFrom(holder, recipient, NFT0, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); - 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'; + this.settings = { + proposal: [ + [ this.receiver.address ], + [ web3.utils.toWei('0') ], + [ this.receiver.contract.methods.mockFunction().encodeABI() ], + '', + ], + tokenHolder: owner, + voters: [ + { voter: voter1, nfts: [NFT0], support: Enums.VoteType.For }, + { voter: voter2, nfts: [NFT1, NFT2], support: Enums.VoteType.For }, + { voter: voter3, nfts: [NFT3], support: Enums.VoteType.Against }, + { voter: voter4, nfts: [NFT4], support: Enums.VoteType.Abstain }, + ], + }; }); afterEach(async function () { - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); - - // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" - const blockNumber = await time.latestBlock(); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getPastVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes); - }); - }); - - // 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, NFT0); - await this.token.mint(holder, NFT1); - await this.token.mint(holder, NFT2); - await this.token.mint(holder, NFT3); - }); - - describe('balanceOf', function () { - it('grants to initial account', async function () { - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('4'); - }); - }); - - describe('numCheckpoints', function () { - it('returns the number of checkpoints for a delegate', async function () { + expect(await this.mock.hasVoted(this.id, owner)).to.be.equal(false); - await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability - await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - - const t1 = await this.token.delegate(other1, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - - const t3 = await this.token.transferFrom(recipient, other2, NFT2, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); + for (const vote of this.receipts.castVote.filter(Boolean)) { + const { voter } = vote.logs.find(Boolean).args; - const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); + expect(await this.mock.hasVoted(this.id, voter)).to.be.equal(true); - expect(await this.token.checkpointAt(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); - expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); - expect(await this.token.checkpointAt(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); - expect(await this.token.checkpointAt(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); - - await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('2'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('1'); - }); - - it('does not add more than one checkpoint in a block', async function () { - await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); - await this.token.transferFrom(holder, recipient, NFT2, { 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.transferFrom(recipient, other2, NFT1, { from: recipient, gas: 200000 }), - () => this.token.transferFrom(recipient, other2, NFT2, { from: recipient, gas: 200000 }), - ]); - - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpointAt(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); - - const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); - }); - }); - - describe('getPastVotes', function () { - it('reverts if block number >= current block', async function () { - await expectRevert( - this.token.getPastVotes(other1, 5e10), - 'block not yet mined', + expectEvent( + vote, + 'VoteCast', + this.settings.voters.find(({ address }) => address === voter), ); - }); - it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0'); - }); - - it('returns the latest block if >= last checkpoint block', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('4'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); - }); + if (voter === voter2) { + expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('2'); + } else { + expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); + } + } - it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); + await this.mock.proposalVotes(this.id).then(result => { + for (const [key, value] of Object.entries(Enums.VoteType)) { + expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( + Object.values(this.settings.voters).filter(({ support }) => support === value).reduce( + (acc, { nfts }) => acc.add(new BN(nfts.length)), + new BN('0'), + ), + ); + } }); - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const total = await this.token.balanceOf(holder); - - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.transferFrom(holder, other2, NFT1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.transferFrom(holder, other2, NFT2, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.transferFrom(other2, holder, NFT2, { from: other2 }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal(total); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(total); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('3'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('2'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); - }); + expect(await this.mock.state(this.id)).to.be.bignumber.equal(ProposalState.Executed); }); - }); - describe('getPastVotingPower', function () { - beforeEach(async function () { - await this.token.delegate(holder, { from: holder }); - }); - - it('reverts if block number >= current block', async function () { - await expectRevert( - this.token.getPastVotingPower(5e10), - 'ERC721Votes: block not yet mined', - ); - }); - - it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastVotingPower(0)).to.be.bignumber.equal('0'); - }); - - it('returns the latest block if >= last checkpoint block', async function () { - t1 = await this.token.mint(holder, NFT0); - - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - }); - - it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const t1 = await this.token.mint(holder, NFT0); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.mint(holder, NFT1); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.burn(NFT1); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.mint(holder, NFT2); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.burn(NFT2); - await time.advanceBlock(); - await time.advanceBlock(); - const t5 = await this.token.mint(holder, NFT3); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotingPower(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotingPower(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotingPower(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotingPower(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotingPower(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotingPower(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotingPower(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotingPower(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - }); + runGovernorWorkflow(); }); }); From 98a6618d422e8e4fa0a6bee75e1feb13dbf79446 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 3 Nov 2021 14:46:27 -0400 Subject: [PATCH 229/300] Updating contracts listing order --- docs/modules/ROOT/pages/governance.adoc | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 28c607e703a..e951cfccd32 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -14,10 +14,6 @@ This governance protocol is generally implemented in a special-purpose contract OpenZeppelin’s Governor system was designed with a concern for compatibility with existing systems that were based on Compound’s GovernorAlpha and GovernorBravo. Because of this, you will find that many modules are presented in two variants, one of which is built for compatibility with those systems. -=== ERC721Votes - -The ERC721 extension to keep track of votes and vote delegation is one such case. - === ERC20Votes & ERC20VotesComp The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. From ef43fb205b27fcb3a9be223b487114d64eb4d988 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 13:00:30 -0400 Subject: [PATCH 230/300] Update erc721.adoc "Voting rights" are generally fungible --- docs/modules/ROOT/pages/erc721.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/erc721.adoc b/docs/modules/ROOT/pages/erc721.adoc index 14dbdc97606..8d28fad2e6e 100644 --- a/docs/modules/ROOT/pages/erc721.adoc +++ b/docs/modules/ROOT/pages/erc721.adoc @@ -1,6 +1,6 @@ = ERC721 -We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate*, *voting rights*, or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. +We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate* or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. ERC721 is a more complex standard than ERC20, with multiple optional extensions, and is split across a number of contracts. The OpenZeppelin Contracts provide flexibility regarding how these are combined, along with custom useful extensions. Check out the xref:api:token/ERC721.adoc[API Reference] to learn more about these. From ec7e89203c9740ae0da5d7fb0da29683688b0c3f Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 25 Nov 2021 09:41:19 -0400 Subject: [PATCH 231/300] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e582c71bdd4..67b0e9494ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ## 4.4.0 (2021-11-25) +## Unreleased + * `Voting`: Create library to be use for ERC721 and ERC1155 voting ([#2944](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2944)) * `Ownable`: add an internal `_transferOwnership(address)`. ([#2568](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2568)) * `AccessControl`: add internal `_grantRole(bytes32,address)` and `_revokeRole(bytes32,address)`. ([#2568](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2568)) * `AccessControl`: mark `_setupRole(bytes32,address)` as deprecated in favor of `_grantRole(bytes32,address)`. ([#2568](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2568)) From b813f9c371c0db275c8594b5008073afd4ff3578 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 26 Nov 2021 16:50:34 -0400 Subject: [PATCH 232/300] Run prettier --- contracts/mocks/CheckpointsImpl.sol | 2 +- contracts/mocks/VotingImpl.sol | 21 +- .../token/ERC721/extensions/ERC721Votes.sol | 328 ------------------ contracts/utils/Checkpoints.sol | 35 +- contracts/utils/Voting.sol | 137 ++++++-- 5 files changed, 142 insertions(+), 381 deletions(-) delete mode 100644 contracts/token/ERC721/extensions/ERC721Votes.sol diff --git a/contracts/mocks/CheckpointsImpl.sol b/contracts/mocks/CheckpointsImpl.sol index e5e0f92cd6c..ee409a45055 100644 --- a/contracts/mocks/CheckpointsImpl.sol +++ b/contracts/mocks/CheckpointsImpl.sol @@ -26,6 +26,6 @@ contract CheckpointsImpl { } function push(uint256 value) public returns (uint256, uint256) { - return _totalCheckpoints.push(value); + return _totalCheckpoints.push(value); } } diff --git a/contracts/mocks/VotingImpl.sol b/contracts/mocks/VotingImpl.sol index 6dd7990b97c..83a569ec31d 100644 --- a/contracts/mocks/VotingImpl.sol +++ b/contracts/mocks/VotingImpl.sol @@ -19,7 +19,7 @@ contract VotingImpl { function getTotalAccountVotesAt(address account, uint32 pos) public view returns (Checkpoints.Checkpoint memory) { return _votes.getTotalAccountVotesAt(account, pos); - } + } function getTotalVotes() public view returns (uint256) { return _votes.getTotalVotes(); @@ -27,7 +27,7 @@ contract VotingImpl { function getTotalAccountVotes(address account) public view returns (uint256) { return _votes.getTotalAccountVotes(account); - } + } function getTotalVotesAt(uint256 timestamp) public view returns (uint256) { return _votes.getTotalVotesAt(timestamp); @@ -37,7 +37,11 @@ contract VotingImpl { return _votes.delegates(account); } - function delegate(address account, address newDelegation, uint256 balance) public { + function delegate( + address account, + address newDelegation, + uint256 balance + ) public { return _votes.delegate(account, newDelegation, balance); } @@ -46,11 +50,14 @@ contract VotingImpl { } function burn(address from, uint256 amount) public { - return _votes.burn(from, amount); + return _votes.burn(from, amount); } - function transfer(address from, address to, uint256 amount) public { - return _votes.transfer(from, to, amount); + function transfer( + address from, + address to, + uint256 amount + ) public { + return _votes.transfer(from, to, amount); } - } diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol deleted file mode 100644 index 3756d1e2e82..00000000000 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ /dev/null @@ -1,328 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/ERC721Votes.sol) - -pragma solidity ^0.8.0; - -import "../ERC721.sol"; -import "../../../utils/Counters.sol"; -import "../../../utils/math/Math.sol"; -import "../../../utils/math/SafeCast.sol"; -import "../../../utils/cryptography/ECDSA.sol"; -import "../../../utils/cryptography/draft-EIP712.sol"; -/** - * @dev Extension of ERC721 to support Compound-like voting and delegation. This version is more generic than Compound's, - * and supports token supply up to 2^224^ - 1, while COMP is limited to 2^96^ - 1. - * - * This 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 {getVotes} and {getPastVotes}. - * - * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it - * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. - * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this - * will significantly increase the base gas cost of transfers. - * - * _Available since v4.2._ - */ -abstract contract ERC721Votes is ERC721, EIP712 { - using Counters for Counters.Counter; - - struct Checkpoint { - uint32 fromBlock; - uint224 votes; - } - uint256 _totalSupply; - bytes32 private constant _DELEGATION_TYPEHASH = - keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); - - mapping(address => address) private _delegates; - mapping(address => Counters.Counter) private _nonces; - mapping(address => Checkpoint[]) private _checkpoints; - Checkpoint[] private _totalSupplyCheckpoints; - - /** - * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. - * - * It's a good idea to use the same `name` that is defined as the ERC721 token name. -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD - */ -======= - - constructor(string memory name, string memory symbol) ERC721(name,symbol) EIP712(name, "1") {}*/ ->>>>>>> Updating tests based on new contract changes -======= - */ ->>>>>>> Governance adocs update -======= - */ ->>>>>>> Governance adocs update - - /** - * @dev Emitted when an account changes their delegate. - */ - event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); - - /** - * @dev Emitted when a token transfer or delegate change results in changes to an account's voting power. - */ - event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); - - /** - * @dev Get the `pos`-th checkpoint for `account`. - */ - function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoint memory) { - return _checkpoints[account][pos]; - } - - /** - * @dev Get number of checkpoints for `account`. - */ - function numCheckpoints(address account) public view virtual returns (uint32) { - return SafeCast.toUint32(_checkpoints[account].length); - } - - /** - * @dev Get the address `account` is currently delegating to. - */ - function delegates(address account) public view virtual returns (address) { - return _delegates[account]; - } - - /** - * @dev Gets the current votes balance for `account` - */ - function getVotes(address account) public view returns (uint256) { - uint256 pos = _checkpoints[account].length; - return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes; - } - - /** - * @dev Retrieve the number of votes for `account` at the end of `blockNumber`. - * - * Requirements: - * - * - `blockNumber` must have been already mined - */ - function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) { - require(blockNumber < block.number, "ERC721Votes: block not yet mined"); - return _checkpointsLookup(_checkpoints[account], blockNumber); - } - - /** - * @dev Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances. - * It is but NOT the sum of all the delegated votes! - * - * Requirements: - * - * - `blockNumber` must have been already mined - */ - function getPastTotalSupply(uint256 blockNumber) public view returns (uint256) { - require(blockNumber < block.number, "ERC721Votes: block not yet mined"); - return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber); - } - - /** - * @dev Lookup a value in a list of (sorted) checkpoints. - */ - function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) private view returns (uint256) { - // We run a binary search to look for the earliest checkpoint taken after `blockNumber`. - // - // During the loop, the index of the wanted checkpoint remains in the range [low-1, high). - // With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant. - // - If the middle checkpoint is after `blockNumber`, we look in [low, mid) - // - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high) - // Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not - // out of bounds (in which case we're looking too far in the past and the result is 0). - // Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is - // past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out - // the same. - uint256 high = ckpts.length; - uint256 low = 0; - while (low < high) { - uint256 mid = Math.average(low, high); - if (ckpts[mid].fromBlock > blockNumber) { - high = mid; - } else { - low = mid + 1; - } - } - - return high == 0 ? 0 : ckpts[high - 1].votes; - } - - /** - * @dev Delegate votes from the sender to `delegatee`. - */ - function delegate(address delegatee) public virtual { - _delegate(_msgSender(), delegatee); - } - - /** - * @dev Delegates votes from signer to `delegatee` - */ - function delegateBySig( - address delegatee, - uint256 nonce, - uint256 expiry, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual { - require(block.timestamp <= expiry, "ERC721Votes: signature expired"); - address signer = ECDSA.recover( - _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), - v, - r, - s - ); - require(nonce == _useNonce(signer), "ERC721Votes: invalid nonce"); - _delegate(signer, delegatee); - } - - /** - * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). - */ - function _maxSupply() internal view virtual returns (uint224) { - return type(uint224).max; - } - - /** - * @dev Snapshots the totalSupply after it has been increased. - */ - function _mint(address account, uint256 tokenId) internal virtual override { - require(_totalSupply + 1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - - super._mint(account, tokenId); - _totalSupply += 1; - - _writeCheckpoint(_totalSupplyCheckpoints, _add, 1); - } - - /** - * @dev Snapshots the totalSupply after it has been decreased. - */ - function _burn(uint256 tokenId) internal virtual override { - super._burn(tokenId); -<<<<<<< HEAD -======= - _totalSupply -= 1; ->>>>>>> Updating tests based on new contract changes - _writeCheckpoint(_totalSupplyCheckpoints, _subtract, 1); - } - - /** - * @dev Move voting power when tokens are transferred. - * - * Emits a {DelegateVotesChanged} event. - */ - function _afterTokenTransfer( - address from, -<<<<<<< HEAD - address to - ) internal virtual { -======= - address to, - uint256 tokenId - ) internal virtual override{ ->>>>>>> Updating tests based on new contract changes - _moveVotingPower(delegates(from), delegates(to), 1); - } - - /** - * @dev Change delegation for `delegator` to `delegatee`. - * - * Emits events {DelegateChanged} and {DelegateVotesChanged}. - */ - function _delegate(address delegator, address delegatee) internal virtual { - address currentDelegate = delegates(delegator); - uint256 delegatorBalance = balanceOf(delegator); - _delegates[delegator] = delegatee; - - emit DelegateChanged(delegator, currentDelegate, delegatee); - - _moveVotingPower(currentDelegate, delegatee, delegatorBalance); - } - - function _moveVotingPower( - address src, - address dst, - uint256 amount - ) private { - if (src != dst && amount > 0) { - if (src != address(0)) { - (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[src], _subtract, amount); - emit DelegateVotesChanged(src, oldWeight, newWeight); - } - - if (dst != address(0)) { - (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[dst], _add, amount); - emit DelegateVotesChanged(dst, oldWeight, newWeight); - } - } - } - - function _writeCheckpoint( - Checkpoint[] storage ckpts, - function(uint256, uint256) view returns (uint256) op, - uint256 delta - ) private returns (uint256 oldWeight, uint256 newWeight) { - uint256 pos = ckpts.length; - oldWeight = pos == 0 ? 0 : ckpts[pos - 1].votes; - newWeight = op(oldWeight, delta); - - if (pos > 0 && ckpts[pos - 1].fromBlock == block.number) { - ckpts[pos - 1].votes = SafeCast.toUint224(newWeight); - } else { - ckpts.push(Checkpoint({fromBlock: SafeCast.toUint32(block.number), votes: SafeCast.toUint224(newWeight)})); - } - } - - /** - * @dev "Consume a nonce": return the current value and increment. - * - * _Available since v4.1._ - */ - function _useNonce(address owner) internal virtual returns (uint256 current) { - Counters.Counter storage nonce = _nonces[owner]; - current = nonce.current(); - nonce.increment(); - } - - /** - * @dev Returns an address nonce. - */ - function nonces(address owner) public view virtual returns (uint256) { - return _nonces[owner].current(); - } - - /** - * @dev Returns DOMAIN_SEPARATOR. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view returns (bytes32) { - return _domainSeparatorV4(); - } - - function _add(uint256 a, uint256 b) private pure returns (uint256) { - return a + b; - } - - function _subtract(uint256 a, uint256 b) private pure returns (uint256) { - return a - b; - } - - /** - * @dev Moves token from the caller's account to `recipient`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address recipient, uint256 tokenId) external returns (bool){ - _transfer(_msgSender(), recipient, tokenId); - _afterTokenTransfer(_msgSender(), recipient); - return true; - } -} diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index 3e34f1f6933..dacd0649e4f 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -18,30 +18,30 @@ library Checkpoints { } /** - * @dev Returns checkpoints length. - */ + * @dev Returns checkpoints length. + */ function length(History storage self) internal view returns (uint256) { return self._checkpoints.length; } /** - * @dev Returns checkpoints at given position. - */ + * @dev Returns checkpoints at given position. + */ function at(History storage self, uint256 pos) internal view returns (Checkpoint memory) { return self._checkpoints[pos]; } /** - * @dev Returns total amount of checkpoints. - */ + * @dev Returns total amount of checkpoints. + */ function latest(History storage self) internal view returns (uint256) { uint256 pos = length(self); return pos == 0 ? 0 : at(self, pos - 1).value; } /** - * @dev Returns checkpoints at given block number. - */ + * @dev Returns checkpoints at given block number. + */ function past(History storage self, uint256 index) internal view returns (uint256) { require(index < block.number, "block not yet mined"); @@ -59,25 +59,24 @@ library Checkpoints { } /** - * @dev Creates checkpoint - */ - function push( - History storage self, - uint256 value - ) internal returns (uint256, uint256) { + * @dev Creates checkpoint + */ + function push(History storage self, uint256 value) internal returns (uint256, uint256) { uint256 pos = length(self); - uint256 old = latest(self); + uint256 old = latest(self); if (pos > 0 && self._checkpoints[pos - 1].index == block.number) { self._checkpoints[pos - 1].value = SafeCast.toUint224(value); } else { - self._checkpoints.push(Checkpoint({ index: SafeCast.toUint32(block.number), value: SafeCast.toUint224(value) })); + self._checkpoints.push( + Checkpoint({index: SafeCast.toUint32(block.number), value: SafeCast.toUint224(value)}) + ); } return (old, value); } /** - * @dev Creates checkpoint - */ + * @dev Creates checkpoint + */ function push( History storage self, function(uint256, uint256) view returns (uint256) op, diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol index 3a60a7f145b..24d70be1635 100644 --- a/contracts/utils/Voting.sol +++ b/contracts/utils/Voting.sol @@ -2,47 +2,93 @@ pragma solidity ^0.8.0; import "./Checkpoints.sol"; -import "hardhat/console.sol"; +/** + * @dev Voting operations. + */ library Voting { using Checkpoints for Checkpoints.History; struct Votes { - mapping(address => address) _delegation; + mapping(address => address) _delegation; mapping(address => Checkpoints.History) _userCheckpoints; - Checkpoints.History _totalCheckpoints; + Checkpoints.History _totalCheckpoints; } + /** + * @dev Returns total amount of votes for account. + */ function getVotes(Votes storage self, address account) internal view returns (uint256) { return self._userCheckpoints[account].latest(); } - function getVotesAt(Votes storage self, address account, uint256 timestamp) internal view returns (uint256) { + /** + * @dev Returns total amount of votes at given position. + */ + function getVotesAt( + Votes storage self, + address account, + uint256 timestamp + ) internal view returns (uint256) { return self._userCheckpoints[account].past(timestamp); } + /** + * @dev Get checkpoint for `account` for specific position. + */ + function getTotalAccountVotesAt( + Votes storage self, + address account, + uint32 pos + ) internal view returns (Checkpoints.Checkpoint memory) { + return self._userCheckpoints[account].at(pos); + } + + /** + * @dev Returns total amount of votes. + */ function getTotalVotes(Votes storage self) internal view returns (uint256) { return self._totalCheckpoints.latest(); } - + + /** + * @dev Get number of checkpoints for `account` including delegation. + */ function getTotalAccountVotes(Votes storage self, address account) internal view returns (uint256) { return self._userCheckpoints[account].length(); - } + } + /** + * @dev Returns all votes for timestamp. + */ function getTotalVotesAt(Votes storage self, uint256 timestamp) internal view returns (uint256) { return self._totalCheckpoints.past(timestamp); } + /** + * @dev Returns account delegation. + */ function delegates(Votes storage self, address account) internal view returns (address) { return self._delegation[account]; } - function delegate(Votes storage self, address account, address newDelegation, uint256 balance) internal { + /** + * @dev Delegates voting power. + */ + function delegate( + Votes storage self, + address account, + address newDelegation, + uint256 balance + ) internal { address oldDelegation = delegates(self, account); self._delegation[account] = newDelegation; - moveVotingPower(self, oldDelegation, newDelegation, balance, _dummy); + _moveVotingPower(self, oldDelegation, newDelegation, balance, _dummy); } + /** + * @dev Delegates voting power. + */ function delegate( Votes storage self, address account, @@ -52,47 +98,74 @@ library Voting { ) internal { address oldDelegation = delegates(self, account); self._delegation[account] = newDelegation; - moveVotingPower(self, oldDelegation, newDelegation, balance, hookDelegateVotesChanged); + _moveVotingPower(self, oldDelegation, newDelegation, balance, hookDelegateVotesChanged); } - function mint(Votes storage self, address to, uint256 amount) internal { - console.log("minting 1"); + /** + * @dev Mints new vote. + */ + function mint( + Votes storage self, + address to, + uint256 amount + ) internal { self._totalCheckpoints.push(_add, amount); - moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); + _moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); } + /** + * @dev Mints new vote. + */ function mint( Votes storage self, address to, uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) internal { - console.log("minting 2"); self._totalCheckpoints.push(_add, amount); - moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); + _moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); } - function burn(Votes storage self, address from, uint256 amount) internal { - console.log("burn 1"); + /** + * @dev Burns new vote. + */ + function burn( + Votes storage self, + address from, + uint256 amount + ) internal { self._totalCheckpoints.push(_subtract, amount); - moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); + _moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); } + /** + * @dev Burns new vote. + */ function burn( Votes storage self, address from, uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) internal { - console.log("burn 2"); self._totalCheckpoints.push(_subtract, amount); - moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); + _moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); } - function transfer(Votes storage self, address from, address to, uint256 amount) internal { - moveVotingPower(self, delegates(self, from), delegates(self, to), amount, _dummy); + /** + * @dev Transfers voting power. + */ + function transfer( + Votes storage self, + address from, + address to, + uint256 amount + ) internal { + _moveVotingPower(self, delegates(self, from), delegates(self, to), amount, _dummy); } + /** + * @dev Transfers voting power. + */ function transfer( Votes storage self, address from, @@ -100,18 +173,19 @@ library Voting { uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) internal { - moveVotingPower(self, delegates(self, from), delegates(self, to), amount, hookDelegateVotesChanged); + _moveVotingPower(self, delegates(self, from), delegates(self, to), amount, hookDelegateVotesChanged); } - function _moveVotingPower( + /** + * @dev Moves voting power. + */ + function _moveVotingPower( Votes storage self, address src, address dst, uint256 amount, function(address, uint256, uint256) hookDelegateVotesChanged ) private { - console.log("moving voting power"); - console.log(block.number); if (src != dst && amount > 0) { if (src != address(0)) { (uint256 oldValue, uint256 newValue) = self._userCheckpoints[src].push(_subtract, amount); @@ -124,14 +198,23 @@ library Voting { } } + /** + * @dev Adds two numbers. + */ function _add(uint256 a, uint256 b) private pure returns (uint256) { return a + b; } + /** + * @dev Subtracts two numbers. + */ function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } - function _dummy(address, uint256, uint256) private pure {} - + function _dummy( + address, + uint256, + uint256 + ) private pure {} } From 09da50deef8a5d9cb31836ef26ec6b669771cc4e Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 29 Nov 2021 13:33:30 -0400 Subject: [PATCH 233/300] Update changelog after merge --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b0e9494ef..9c13adfe6b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,13 @@ ## Unreleased +* `Voting`: Create library to be use for ERC721 and ERC1155 voting ([#2944](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2944)) * `GovernorTimelockControl`: improve the `state()` function to have it reflect cases where a proposal has been canceled directly on the timelock. ([#2977](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2977)) * `Math`: add a `abs(int256)` method that returns the unsigned absolute value of a signed value. ([#2984](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2984)) * Preset contracts are now deprecated in favor of [Contracts Wizard](https://wizard.openzeppelin.com). ([#2986](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2986)) ## 4.4.0 (2021-11-25) -## Unreleased - * `Voting`: Create library to be use for ERC721 and ERC1155 voting ([#2944](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2944)) * `Ownable`: add an internal `_transferOwnership(address)`. ([#2568](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2568)) * `AccessControl`: add internal `_grantRole(bytes32,address)` and `_revokeRole(bytes32,address)`. ([#2568](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2568)) * `AccessControl`: mark `_setupRole(bytes32,address)` as deprecated in favor of `_grantRole(bytes32,address)`. ([#2568](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2568)) From e0e8d7442cf07cd8a0304267b8a16188b69fe64e Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 29 Nov 2021 16:18:34 -0400 Subject: [PATCH 234/300] Update after rebase --- .../extensions/GovernorVotesERC721.sol | 9 +- contracts/mocks/ERC721PermitMock.sol | 20 -- contracts/mocks/ERC721VotesMock.sol | 4 - .../ERC721/extensions/draft-ERC721Votes.sol | 225 ++++++++++++++++++ .../ERC721/extensions/ERC721Votes.test.js | 18 +- .../extensions/draft-ERC721Permit.test.js | 117 --------- test/utils/Checkpoints.test.js | 41 ++++ test/utils/Voting.test.js | 64 +++++ 8 files changed, 345 insertions(+), 153 deletions(-) delete mode 100644 contracts/mocks/ERC721PermitMock.sol create mode 100644 contracts/token/ERC721/extensions/draft-ERC721Votes.sol delete mode 100644 test/token/ERC721/extensions/draft-ERC721Permit.test.js create mode 100644 test/utils/Checkpoints.test.js create mode 100644 test/utils/Voting.test.js diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 2e3079fc243..29050b1d121 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -4,23 +4,26 @@ pragma solidity ^0.8.0; import "../Governor.sol"; -import "../../token/ERC721/extensions/ERC721Votes.sol"; +import "../../token/ERC721/extensions/draft-ERC721Votes.sol"; import "../../utils/math/Math.sol"; /** * @dev Extension of {Governor} for voting weight extraction from an {ERC721Votes} token. * - * _Available since v4.3._ + * _Available since v4.5._ */ abstract contract GovernorVotesERC721 is Governor { ERC721Votes public immutable token; + /** + * @dev Need the ERC721Votes address to be initialized + */ constructor(ERC721Votes tokenAddress) { token = tokenAddress; } /** - * Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). + * @dev Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). */ function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { return token.getPastVotes(account, blockNumber); diff --git a/contracts/mocks/ERC721PermitMock.sol b/contracts/mocks/ERC721PermitMock.sol deleted file mode 100644 index 37a860ef4dc..00000000000 --- a/contracts/mocks/ERC721PermitMock.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../token/ERC721/extensions/draft-ERC721Permit.sol"; - -contract ERC721PermitMock is ERC721Permit { - constructor( - string memory name, - string memory symbol, - address initialAccount, - uint256 tokenId - ) payable ERC721(name, symbol) ERC721Permit(name) { - _mint(initialAccount, tokenId); - } - - function getChainId() external view returns (uint256) { - return block.chainid; - } -} diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 13dd53a85ae..b47bfd8f24e 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,11 +5,7 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/draft-ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { -<<<<<<< HEAD constructor(string memory name, string memory symbol) ERC721Votes(name, symbol) {} -======= - constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} ->>>>>>> Governance adocs update function mint(address account, uint256 tokenId) public { _mint(account, tokenId); diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol new file mode 100644 index 00000000000..2c2b2ff7ee0 --- /dev/null +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Votes.sol) + +pragma solidity ^0.8.0; + +import "../ERC721.sol"; +import "../../../utils/Voting.sol"; +import "../../../utils/Counters.sol"; +import "../../../utils/math/Math.sol"; +import "../../../utils/Checkpoints.sol"; +import "../../../utils/math/SafeCast.sol"; +import "../../../utils/cryptography/ECDSA.sol"; +import "../../../utils/cryptography/draft-EIP712.sol"; + +/** + * @dev Extension of ERC721 to support Compound-like voting and delegation. This version is more generic than Compound's, + * and supports token supply up to 2^224^ - 1, while COMP is limited to 2^96^ - 1. + * + * This 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 {getVotes} and {getPastVotes}. + * + * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it + * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. + * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this + * will significantly increase the base gas cost of transfers. + * + * _Available since v4.5._ + */ +abstract contract ERC721Votes is ERC721, EIP712 { + using Counters for Counters.Counter; + using Voting for Voting.Votes; + + uint256 _totalVotingPower; + bytes32 private constant _DELEGATION_TYPEHASH = + keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + + Voting.Votes private _votes; + mapping(address => Counters.Counter) private _nonces; + + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC721 token name. + */ + constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} + + /** + * @dev Emitted when an account changes their delegate. + */ + event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); + + /** + * @dev Emitted when a token transfer or delegate change results in changes to an account's voting power. + */ + event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); + + /** + * @dev Get number of checkpoints for `account`. + */ + function numCheckpoints(address account) public view virtual returns (uint32) { + return SafeCast.toUint32(_votes.getTotalAccountVotes(account)); + } + + /** + * @dev Get the `pos`-th checkpoint for `account`. + */ + function checkpointAt(address account, uint32 pos) public view virtual returns (Checkpoints.Checkpoint memory) { + return _votes.getTotalAccountVotesAt(account, pos); + } + + /** + * @dev Get the address `account` is currently delegating to. + */ + function delegates(address account) public view virtual returns (address) { + return _votes.delegates(account); + } + + /** + * @dev Gets the current votes balance for `account` + */ + function getVotes(address account) public view returns (uint256) { + return _votes.getVotes(account); + } + + /** + * @dev Retrieve the number of votes for `account` at the end of `blockNumber`. + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) { + return _votes.getVotesAt(account, blockNumber); + } + + /** + * @dev Retrieve the `totalVotingPower` at the end of `blockNumber`. Note, this value is the sum of all balances. + * It is but NOT the sum of all the delegated votes! + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastVotingPower(uint256 blockNumber) public view returns (uint256) { + require(blockNumber < block.number, "ERC721Votes: block not yet mined"); + return _votes.getTotalVotesAt(blockNumber); + } + + /** + * @dev Delegate votes from the sender to `delegatee`. + */ + function delegate(address delegatee) public virtual { + _delegate(_msgSender(), delegatee); + } + + /** + * @dev Delegates votes from signer to `delegatee` + */ + function delegateBySig( + address delegatee, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + require(block.timestamp <= expiry, "ERC721Votes: signature expired"); + address signer = ECDSA.recover( + _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), + v, + r, + s + ); + require(nonce == _useNonce(signer), "ERC721Votes: invalid nonce"); + _delegate(signer, delegatee); + } + + /** + * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). + */ + function _maxSupply() internal view virtual returns (uint224) { + return type(uint224).max; + } + + /** + * @dev Snapshots the totalSupply after it has been increased. + */ + function _mint(address account, uint256 tokenId) internal virtual override { + require(_totalVotingPower + 1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); + + super._mint(account, tokenId); + _totalVotingPower += 1; + + _votes.mint(account, 1, _hookDelegateVotesChanged); + } + + /** + * @dev Snapshots the totalSupply after it has been decreased. + */ + function _burn(uint256 tokenId) internal virtual override { + address from = ownerOf(tokenId); + super._burn(tokenId); + _totalVotingPower -= 1; + _votes.burn(from, 1, _hookDelegateVotesChanged); + } + + /** + * @dev Move voting power when tokens are transferred. + * + * Emits a {DelegateVotesChanged} event. + */ + function _afterTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual override { + super._afterTokenTransfer(from, to, tokenId); + _votes.transfer(from, to, 1, _hookDelegateVotesChanged); + } + + /** + * @dev Change delegation for `delegator` to `delegatee`. + * + * Emits events {DelegateChanged} and {DelegateVotesChanged}. + */ + function _delegate(address delegator, address delegatee) internal virtual { + emit DelegateChanged(delegator, delegates(delegator), delegatee); + _votes.delegate(delegator, delegatee, balanceOf(delegator), _hookDelegateVotesChanged); + } + + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } + + /** + * @dev Returns an address nonce. + */ + function nonces(address owner) public view virtual returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev Returns DOMAIN_SEPARATOR. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparatorV4(); + } + + function _hookDelegateVotesChanged( + address account, + uint256 previousBalance, + uint256 newBalance + ) private { + emit DelegateVotesChanged(account, previousBalance, newBalance); + } +} diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 81397dda8aa..33af029f6ee 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -388,7 +388,6 @@ contract('ERC721Votes', function (accounts) { const t1 = await this.token.delegate(other1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); @@ -398,10 +397,10 @@ contract('ERC721Votes', function (accounts) { const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); + expect(await this.token.checkpointAt(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); + expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); + expect(await this.token.checkpointAt(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); + expect(await this.token.checkpointAt(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); await time.advanceBlock(); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('2'); @@ -420,12 +419,13 @@ contract('ERC721Votes', function (accounts) { () => this.token.transferFrom(recipient, other2, NFT1, { from: recipient, gas: 200000 }), () => this.token.transferFrom(recipient, other2, NFT2, { from: recipient, gas: 200000 }), ]); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); + expect(await this.token.checkpointAt(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); + expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); }); }); @@ -433,7 +433,7 @@ contract('ERC721Votes', function (accounts) { it('reverts if block number >= current block', async function () { await expectRevert( this.token.getPastVotes(other1, 5e10), - 'ERC721Votes: block not yet mined', + 'block not yet mined', ); }); @@ -541,7 +541,7 @@ contract('ERC721Votes', function (accounts) { const t5 = await this.token.mint(holder, NFT3); await time.advanceBlock(); await time.advanceBlock(); - + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); diff --git a/test/token/ERC721/extensions/draft-ERC721Permit.test.js b/test/token/ERC721/extensions/draft-ERC721Permit.test.js deleted file mode 100644 index 7a7b8cf5d83..00000000000 --- a/test/token/ERC721/extensions/draft-ERC721Permit.test.js +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable */ - -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); -const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; - -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; - -const ERC721PermitMock = artifacts.require('ERC721PermitMock'); - -const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); - -const Permit = [ - { name: 'owner', type: 'address' }, - { name: 'spender', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint256' }, -]; - -contract('ERC721Permit', function (accounts) { - const [ initialHolder, spender, recipient, other ] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - - const initialTokenId = new BN('100'); - - beforeEach(async function () { - this.token = await ERC721PermitMock.new(name, symbol, initialHolder, initialTokenId); - - // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id - // from within the EVM as from the JSON RPC interface. - // See https://github.com/trufflesuite/ganache-core/issues/515 - this.chainId = await this.token.getChainId(); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect( - await this.token.DOMAIN_SEPARATOR(), - ).to.equal( - await domainSeparator(name, version, this.chainId, this.token.address), - ); - }); - - describe.only('permit', function () { - const wallet = Wallet.generate(); - - const owner = wallet.getAddressString(); - const value = initialTokenId; - const nonce = 0; - const maxDeadline = MAX_UINT256; - - const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ - primaryType: 'Permit', - types: { EIP712Domain, Permit }, - domain: { name, version, chainId, verifyingContract }, - message: { owner, spender, value, nonce, deadline }, - }); - - it('accepts owner signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - const receipt = await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); - expect(await this.token.getApproved(value)).to.be.equal(spender); - }); - - it('rejects reused signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects other signature', async function () { - const otherWallet = Wallet.generate(); - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC721Permit: invalid signature', - ); - }); - - it('rejects expired permit', async function () { - const deadline = (await time.latest()) - time.duration.weeks(1); - - const data = buildData(this.chainId, this.token.address, deadline); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, deadline, v, r, s), - 'ERC721Permit: expired deadline', - ); - }); - }); -}); diff --git a/test/utils/Checkpoints.test.js b/test/utils/Checkpoints.test.js new file mode 100644 index 00000000000..33ba473946c --- /dev/null +++ b/test/utils/Checkpoints.test.js @@ -0,0 +1,41 @@ +const { expectRevert } = require('@openzeppelin/test-helpers'); + +const { expect } = require('chai'); + +const CheckpointsImpl = artifacts.require('CheckpointsImpl'); + +contract('Checkpoints', function (accounts) { + beforeEach(async function () { + this.checkpoint = await CheckpointsImpl.new(); + this.tx1 = await this.checkpoint.push(1); + this.tx2 = await this.checkpoint.push(2); + this.tx3 = await this.checkpoint.push(3); + }); + + it('calls length', async function () { + expect(await this.checkpoint.length()).to.be.bignumber.equal('3'); + }); + + it('calls at', async function () { + expect((await this.checkpoint.at(0))[1]).to.be.bignumber.equal('1'); + expect((await this.checkpoint.at(1))[1]).to.be.bignumber.equal('2'); + expect((await this.checkpoint.at(2))[1]).to.be.bignumber.equal('3'); + }); + + it('calls latest', async function () { + expect(await this.checkpoint.latest()).to.be.bignumber.equal('3'); + }); + + it('calls past', async function () { + expect(await this.checkpoint.past(this.tx1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.checkpoint.past(this.tx2.receipt.blockNumber - 1)).to.be.bignumber.equal('1'); + expect(await this.checkpoint.past(this.tx3.receipt.blockNumber - 1)).to.be.bignumber.equal('2'); + }); + + it('reverts if block number >= current block', async function () { + await expectRevert( + this.checkpoint.past(this.tx3.receipt.blockNumber + 1), + 'block not yet mined', + ); + }); +}); diff --git a/test/utils/Voting.test.js b/test/utils/Voting.test.js new file mode 100644 index 00000000000..06afa2736ba --- /dev/null +++ b/test/utils/Voting.test.js @@ -0,0 +1,64 @@ +const { expectRevert } = require('@openzeppelin/test-helpers'); + +const { expect } = require('chai'); + +const VotingImp = artifacts.require('VotingImpl'); + +contract('Voting', function (accounts) { + const [ account1, account2, account3 ] = accounts; + beforeEach(async function () { + this.voting = await VotingImp.new(); + }); + + it('starts with zero votes', async function () { + expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('0'); + }); + + describe('move voting power', function () { + beforeEach(async function () { + this.tx1 = await this.voting.mint(account1, 1); + this.tx2 = await this.voting.mint(account2, 1); + this.tx3 = await this.voting.mint(account3, 1); + }); + + it('mints', async function () { + expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('3'); + + expect(await this.voting.getTotalVotesAt(this.tx1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.voting.getTotalVotesAt(this.tx2.receipt.blockNumber - 1)).to.be.bignumber.equal('1'); + expect(await this.voting.getTotalVotesAt(this.tx3.receipt.blockNumber - 1)).to.be.bignumber.equal('2'); + }); + + it('reverts if block number >= current block', async function () { + await expectRevert( + this.voting.getTotalVotesAt(this.tx3.receipt.blockNumber + 1), + 'block not yet mined', + ); + }); + + it('burns', async function () { + await this.voting.burn(account1, 1); + expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('2'); + + await this.voting.burn(account2, 1); + expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('1'); + + await this.voting.burn(account3, 1); + expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('0'); + }); + + it('delegates', async function () { + await this.voting.delegate(account3, account2, 1); + + expect(await this.voting.delegates(account3)).to.be.equal(account2); + }); + + it('transfers', async function () { + await this.voting.delegate(account1, account2, 1); + await this.voting.transfer(account1, account2, 1); + + expect(await this.voting.getTotalAccountVotes(account1)).to.be.bignumber.equal('0'); + expect(await this.voting.getTotalAccountVotes(account2)).to.be.bignumber.equal('2'); + }); + }); +}); From d7f1ddab63c0678a85c3ca808e16c1cc3407ef08 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 29 Nov 2021 16:58:43 -0400 Subject: [PATCH 235/300] Remove unused library --- contracts/governance/extensions/GovernorVotes.sol | 1 - contracts/governance/extensions/GovernorVotesERC721.sol | 1 - 2 files changed, 2 deletions(-) diff --git a/contracts/governance/extensions/GovernorVotes.sol b/contracts/governance/extensions/GovernorVotes.sol index d1719ca149a..86478c0418d 100644 --- a/contracts/governance/extensions/GovernorVotes.sol +++ b/contracts/governance/extensions/GovernorVotes.sol @@ -5,7 +5,6 @@ pragma solidity ^0.8.0; import "../Governor.sol"; import "../../token/ERC20/extensions/ERC20Votes.sol"; -import "../../utils/math/Math.sol"; /** * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token. diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 29050b1d121..2a347ae4c0a 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -5,7 +5,6 @@ pragma solidity ^0.8.0; import "../Governor.sol"; import "../../token/ERC721/extensions/draft-ERC721Votes.sol"; -import "../../utils/math/Math.sol"; /** * @dev Extension of {Governor} for voting weight extraction from an {ERC721Votes} token. From 5506617b0a0c096ecf92f207e309b3e67a71c763 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 29 Nov 2021 17:01:59 -0400 Subject: [PATCH 236/300] Prevent version mismatch --- contracts/governance/extensions/GovernorVotesERC721.sol | 2 +- contracts/token/ERC20/extensions/ERC20Votes.sol | 2 +- contracts/token/ERC721/extensions/draft-ERC721Votes.sol | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol index 2a347ae4c0a..37391e708b7 100644 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ b/contracts/governance/extensions/GovernorVotesERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (governance/extensions/ERC721GovernorVotesERC721.sol) +// OpenZeppelin Contracts v4.4.0 (governance/extensions/ERC721GovernorVotesERC721.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index 5e176973ee2..f4fd21d3e5b 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC20/extensions/ERC20Votes.sol) +// OpenZeppelin Contracts v4.4.0 (token/ERC20/extensions/ERC20Votes.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 2c2b2ff7ee0..6b90691015d 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.3.2 (token/ERC721/extensions/draft-ERC721Votes.sol) +// OpenZeppelin Contracts v4.4.0 (token/ERC721/extensions/draft-ERC721Votes.sol) pragma solidity ^0.8.0; From 424af509d1da106151cf0224e16c99fd51b0a495 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 30 Nov 2021 08:36:01 -0400 Subject: [PATCH 237/300] Remove minting restriction --- contracts/mocks/ERC721VotesMock.sol | 4 ---- contracts/token/ERC721/ERC721.sol | 4 ++-- .../token/ERC721/extensions/draft-ERC721Votes.sol | 5 ----- test/token/ERC721/extensions/ERC721Votes.test.js | 14 -------------- 4 files changed, 2 insertions(+), 25 deletions(-) diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index b47bfd8f24e..cd593bb5437 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -18,8 +18,4 @@ contract ERC721VotesMock is ERC721Votes { function getChainId() external view returns (uint256) { return block.chainid; } - - function _maxSupply() internal pure override returns (uint224) { - return uint224(5); - } } diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 51e9bbd7cae..191e83af804 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -423,8 +423,8 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { address to, uint256 tokenId ) internal virtual {} - - /** + + /** * @dev Hook that is called after any transfer of tokens. This includes * minting and burning. * diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 6b90691015d..91e1c10343a 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -31,7 +31,6 @@ abstract contract ERC721Votes is ERC721, EIP712 { using Counters for Counters.Counter; using Voting for Voting.Votes; - uint256 _totalVotingPower; bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); @@ -147,10 +146,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { * @dev Snapshots the totalSupply after it has been increased. */ function _mint(address account, uint256 tokenId) internal virtual override { - require(_totalVotingPower + 1 <= _maxSupply(), "ERC721Votes: total supply risks overflowing votes"); - super._mint(account, tokenId); - _totalVotingPower += 1; _votes.mint(account, 1, _hookDelegateVotesChanged); } @@ -161,7 +157,6 @@ abstract contract ERC721Votes is ERC721, EIP712 { function _burn(uint256 tokenId) internal virtual override { address from = ownerOf(tokenId); super._burn(tokenId); - _totalVotingPower -= 1; _votes.burn(from, 1, _hookDelegateVotesChanged); } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 33af029f6ee..592cef62c8a 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -84,20 +84,6 @@ contract('ERC721Votes', function (accounts) { ); }); - it('minting restriction', async function () { - const lastTokenId = new BN('2').pow(new BN('224')); - this.token.mint(holder, NFT1); - this.token.mint(holder, NFT2); - this.token.mint(holder, NFT3); - this.token.mint(holder, NFT0); - this.token.mint(holder, NFT4); - - await expectRevert( - this.token.mint(holder, lastTokenId), - 'ERC721Votes: total supply risks overflowing votes', - ); - }); - describe('set delegation', function () { describe('call', function () { it('delegation with tokens', async function () { From b2b7f5bcdf4854db350d9323ccc6dcab63f2e947 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 30 Nov 2021 09:02:43 -0400 Subject: [PATCH 238/300] Remove afterToken transfer and Add _transfer function --- contracts/token/ERC721/extensions/draft-ERC721Votes.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 91e1c10343a..babdc2a1d64 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -161,16 +161,16 @@ abstract contract ERC721Votes is ERC721, EIP712 { } /** - * @dev Move voting power when tokens are transferred. + * @dev Transfers `tokenId` from `from` to `to` and moves voting power when tokens are transferred * - * Emits a {DelegateVotesChanged} event. + * Emits a {Transfer} event. */ - function _afterTokenTransfer( + function _transfer( address from, address to, uint256 tokenId ) internal virtual override { - super._afterTokenTransfer(from, to, tokenId); + super._transfer(from, to, tokenId); _votes.transfer(from, to, 1, _hookDelegateVotesChanged); } From ef252c9cdacd4684ca534bddbdefb51993d2fb2f Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 30 Nov 2021 16:05:49 -0400 Subject: [PATCH 239/300] Remove checkpoints methods --- .../ERC721/extensions/draft-ERC721Votes.sol | 14 ----- .../ERC721/extensions/ERC721Votes.test.js | 60 ++----------------- 2 files changed, 5 insertions(+), 69 deletions(-) diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index babdc2a1d64..1171ae66f11 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -54,20 +54,6 @@ abstract contract ERC721Votes is ERC721, EIP712 { */ event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); - /** - * @dev Get number of checkpoints for `account`. - */ - function numCheckpoints(address account) public view virtual returns (uint32) { - return SafeCast.toUint32(_votes.getTotalAccountVotes(account)); - } - - /** - * @dev Get the `pos`-th checkpoint for `account`. - */ - function checkpointAt(address account, uint32 pos) public view virtual returns (Checkpoints.Checkpoint memory) { - return _votes.getTotalAccountVotesAt(account, pos); - } - /** * @dev Get the address `account` is currently delegating to. */ diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 592cef62c8a..4dc442c46f6 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -53,11 +53,11 @@ async function batchInBlock (txs) { } contract('ERC721Votes', function (accounts) { - const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; + const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; const NFT0 = new BN('10000000000000000000000000'); const NFT1 = new BN('10'); const NFT2 = new BN('20'); - const NFT3 = new BN('30'); + const NFT3 = new BN('30'); const NFT4 = new BN('40'); const name = 'My Token'; const symbol = 'MTKN'; @@ -365,56 +365,6 @@ contract('ERC721Votes', function (accounts) { }); }); - describe('numCheckpoints', function () { - it('returns the number of checkpoints for a delegate', async function () { - - await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); //give an account two tokens for readability - await this.token.transferFrom(holder, recipient, NFT2, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - - const t1 = await this.token.delegate(other1, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - const t2 = await this.token.transferFrom(recipient, other2, NFT1, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - - const t3 = await this.token.transferFrom(recipient, other2, NFT2, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - - const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - - expect(await this.token.checkpointAt(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '2' ]); - expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t2.receipt.blockNumber.toString(), '1' ]); - expect(await this.token.checkpointAt(other1, 2)).to.be.deep.equal([ t3.receipt.blockNumber.toString(), '0' ]); - expect(await this.token.checkpointAt(other1, 3)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); - - await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('2'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('1'); - }); - - it('does not add more than one checkpoint in a block', async function () { - await this.token.transferFrom(holder, recipient, NFT1, { from: holder }); - await this.token.transferFrom(holder, recipient, NFT2, { 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.transferFrom(recipient, other2, NFT1, { from: recipient, gas: 200000 }), - () => this.token.transferFrom(recipient, other2, NFT2, { from: recipient, gas: 200000 }), - ]); - - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpointAt(other1, 0)).to.be.deep.equal([ t1.receipt.blockNumber.toString(), '0' ]); - - const t4 = await this.token.transferFrom(holder, recipient, NFT3, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpointAt(other1, 1)).to.be.deep.equal([ t4.receipt.blockNumber.toString(), '1' ]); - }); - }); - describe('getPastVotes', function () { it('reverts if block number >= current block', async function () { await expectRevert( @@ -451,7 +401,7 @@ contract('ERC721Votes', function (accounts) { const t1 = await this.token.delegate(other1, { from: holder }); await time.advanceBlock(); - await time.advanceBlock(); + await time.advanceBlock(); const t2 = await this.token.transferFrom(holder, other2, NFT1, { from: holder }); await time.advanceBlock(); await time.advanceBlock(); @@ -461,7 +411,7 @@ contract('ERC721Votes', function (accounts) { const t4 = await this.token.transferFrom(other2, holder, NFT2, { from: other2 }); await time.advanceBlock(); await time.advanceBlock(); - + expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal(total); expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(total); @@ -527,7 +477,7 @@ contract('ERC721Votes', function (accounts) { const t5 = await this.token.mint(holder, NFT3); await time.advanceBlock(); await time.advanceBlock(); - + expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); From 83eeac50a0c035b831b6d0a73054f29bcbe6909c Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 1 Dec 2021 09:16:35 -0400 Subject: [PATCH 240/300] Update contract structure --- .../ERC721/extensions/draft-ERC721Votes.sol | 150 +---------- contracts/utils/Votes.sol | 235 ++++++++++++++++++ contracts/utils/Voting.sol | 220 ---------------- 3 files changed, 245 insertions(+), 360 deletions(-) create mode 100644 contracts/utils/Votes.sol delete mode 100644 contracts/utils/Voting.sol diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 1171ae66f11..30a45e01e09 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -4,13 +4,11 @@ pragma solidity ^0.8.0; import "../ERC721.sol"; -import "../../../utils/Voting.sol"; -import "../../../utils/Counters.sol"; +import "../../../utils/Votes.sol"; import "../../../utils/math/Math.sol"; import "../../../utils/Checkpoints.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; -import "../../../utils/cryptography/draft-EIP712.sol"; /** * @dev Extension of ERC721 to support Compound-like voting and delegation. This version is more generic than Compound's, @@ -27,70 +25,7 @@ import "../../../utils/cryptography/draft-EIP712.sol"; * * _Available since v4.5._ */ -abstract contract ERC721Votes is ERC721, EIP712 { - using Counters for Counters.Counter; - using Voting for Voting.Votes; - - bytes32 private constant _DELEGATION_TYPEHASH = - keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); - - Voting.Votes private _votes; - mapping(address => Counters.Counter) private _nonces; - - /** - * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. - * - * It's a good idea to use the same `name` that is defined as the ERC721 token name. - */ - constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} - - /** - * @dev Emitted when an account changes their delegate. - */ - event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); - - /** - * @dev Emitted when a token transfer or delegate change results in changes to an account's voting power. - */ - event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); - - /** - * @dev Get the address `account` is currently delegating to. - */ - function delegates(address account) public view virtual returns (address) { - return _votes.delegates(account); - } - - /** - * @dev Gets the current votes balance for `account` - */ - function getVotes(address account) public view returns (uint256) { - return _votes.getVotes(account); - } - - /** - * @dev Retrieve the number of votes for `account` at the end of `blockNumber`. - * - * Requirements: - * - * - `blockNumber` must have been already mined - */ - function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) { - return _votes.getVotesAt(account, blockNumber); - } - - /** - * @dev Retrieve the `totalVotingPower` at the end of `blockNumber`. Note, this value is the sum of all balances. - * It is but NOT the sum of all the delegated votes! - * - * Requirements: - * - * - `blockNumber` must have been already mined - */ - function getPastVotingPower(uint256 blockNumber) public view returns (uint256) { - require(blockNumber < block.number, "ERC721Votes: block not yet mined"); - return _votes.getTotalVotesAt(blockNumber); - } +abstract contract ERC721Votes is ERC721, Votes { /** * @dev Delegate votes from the sender to `delegatee`. @@ -99,28 +34,6 @@ abstract contract ERC721Votes is ERC721, EIP712 { _delegate(_msgSender(), delegatee); } - /** - * @dev Delegates votes from signer to `delegatee` - */ - function delegateBySig( - address delegatee, - uint256 nonce, - uint256 expiry, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual { - require(block.timestamp <= expiry, "ERC721Votes: signature expired"); - address signer = ECDSA.recover( - _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), - v, - r, - s - ); - require(nonce == _useNonce(signer), "ERC721Votes: invalid nonce"); - _delegate(signer, delegatee); - } - /** * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). */ @@ -134,7 +47,7 @@ abstract contract ERC721Votes is ERC721, EIP712 { function _mint(address account, uint256 tokenId) internal virtual override { super._mint(account, tokenId); - _votes.mint(account, 1, _hookDelegateVotesChanged); + _mintVote(account, 1); } /** @@ -143,64 +56,21 @@ abstract contract ERC721Votes is ERC721, EIP712 { function _burn(uint256 tokenId) internal virtual override { address from = ownerOf(tokenId); super._burn(tokenId); - _votes.burn(from, 1, _hookDelegateVotesChanged); + _burnVote(from, 1); } /** - * @dev Transfers `tokenId` from `from` to `to` and moves voting power when tokens are transferred + * @dev Move voting power when tokens are transferred. * - * Emits a {Transfer} event. + * Emits a {DelegateVotesChanged} event. */ - function _transfer( + function _afterTokenTransfer( address from, address to, uint256 tokenId - ) internal virtual override { - super._transfer(from, to, tokenId); - _votes.transfer(from, to, 1, _hookDelegateVotesChanged); - } - - /** - * @dev Change delegation for `delegator` to `delegatee`. - * - * Emits events {DelegateChanged} and {DelegateVotesChanged}. - */ - function _delegate(address delegator, address delegatee) internal virtual { - emit DelegateChanged(delegator, delegates(delegator), delegatee); - _votes.delegate(delegator, delegatee, balanceOf(delegator), _hookDelegateVotesChanged); - } - - /** - * @dev "Consume a nonce": return the current value and increment. - * - * _Available since v4.1._ - */ - function _useNonce(address owner) internal virtual returns (uint256 current) { - Counters.Counter storage nonce = _nonces[owner]; - current = nonce.current(); - nonce.increment(); - } - - /** - * @dev Returns an address nonce. - */ - function nonces(address owner) public view virtual returns (uint256) { - return _nonces[owner].current(); - } - - /** - * @dev Returns DOMAIN_SEPARATOR. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view returns (bytes32) { - return _domainSeparatorV4(); - } + ) internal virtual override{ + super._afterTokenTransfer(from, to, tokenId); - function _hookDelegateVotesChanged( - address account, - uint256 previousBalance, - uint256 newBalance - ) private { - emit DelegateVotesChanged(account, previousBalance, newBalance); + _moveVotingPower(_delegates(from), _delegates(to), 1); } } diff --git a/contracts/utils/Votes.sol b/contracts/utils/Votes.sol new file mode 100644 index 00000000000..2e23b718ded --- /dev/null +++ b/contracts/utils/Votes.sol @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./Counters.sol"; +import "./Checkpoints.sol"; +import "./cryptography/draft-EIP712.sol"; + +/** + * @dev Voting operations. + */ +abstract contract Votes is EIP712 { + using Checkpoints for Checkpoints.History; + using Counters for Counters.Counter; + + bytes32 private constant _DELEGATION_TYPEHASH = + keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + mapping(address => address) _delegation; + mapping(address => Checkpoints.History) _userCheckpoints; + mapping(address => Counters.Counter) private _nonces; + Checkpoints.History _totalCheckpoints; + + /** + * @dev Emitted when an account changes their delegate. + */ + event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); + + /** + * @dev Emitted when a token transfer or delegate change results in changes to an account's voting power. + */ + event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); + + /** + * @dev Returns total amount of votes for account. + */ + function _getVotes(address account) internal view returns (uint256) { + return _userCheckpoints[account].latest(); + } + + /** + * @dev Returns total amount of votes at given position. + */ + function _getPastVotes( + address account, + uint256 timestamp + ) internal view returns (uint256) { + return _userCheckpoints[account].past(timestamp); + } + + /** + * @dev Retrieve the `totalVotingPower` at the end of `blockNumber`. Note, this value is the sum of all balances. + * It is but NOT the sum of all the delegated votes! + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastTotalSupply(uint256 blockNumber) public view returns (uint256) { + require(blockNumber < block.number, "ERC721Votes: block not yet mined"); + return _totalCheckpoints.past(blockNumber); + } + + /** + * @dev Get checkpoint for `account` for specific position. + */ + function _getTotalAccountVotesAt( + address account, + uint32 pos + ) internal view returns (Checkpoints.Checkpoint memory) { + return _userCheckpoints[account].at(pos); + } + + /** + * @dev Returns total amount of votes. + */ + function _getTotalVotes() internal view returns (uint256) { + return _totalCheckpoints.latest(); + } + + /** + * @dev Get number of checkpoints for `account` including delegation. + */ + function _getTotalAccountVotes(address account) internal view returns (uint256) { + return _userCheckpoints[account].length(); + } + + /** + * @dev Change delegation for `delegator` to `delegatee`. + * + * Emits events {DelegateChanged} and {DelegateVotesChanged}. + */ + function _delegate(address delegator, address delegatee) internal virtual { + emit DelegateChanged(delegator, _delegates(delegator), delegatee); + _delegate(delegator, delegatee, _getDelegatorVotes(delegator)); + } + + /** + * @dev Returns account delegation. + */ + function _delegates(address account) internal view returns (address) { + return _delegation[account]; + } + + /** + * @dev Delegates voting power. + */ + function _delegate( + address account, + address newDelegation, + uint256 balance + ) internal { + address oldDelegation = _delegates(account); + _delegation[account] = newDelegation; + + emit DelegateChanged(account, oldDelegation, newDelegation); + + _moveVotingPower(oldDelegation, newDelegation, balance); + } + + /** + * @dev Mints new vote. + */ + function _mintVote( + address to, + uint256 amount + ) internal { + _totalCheckpoints.push(_add, amount); + _moveVotingPower(address(0), _delegates(to), amount); + } + + /** + * @dev Burns new vote. + */ + function _burnVote( + address from, + uint256 amount + ) internal { + _totalCheckpoints.push(_subtract, amount); + _moveVotingPower(_delegates(from), address(0), amount); + } + + /** + * @dev Transfers voting power. + */ + function _transferVote( + address from, + address to, + uint256 amount + ) internal { + _moveVotingPower(_delegates(from), _delegates(to), amount); + } + + /** + * @dev Moves voting power. + */ + function _moveVotingPower( + address src, + address dst, + uint256 amount + ) internal { + if (src != dst && amount > 0) { + if (src != address(0)) { + (uint256 oldValue, uint256 newValue) = _userCheckpoints[src].push(_subtract, amount); + emit DelegateVotesChanged(src, oldValue, newValue); + } + if (dst != address(0)) { + (uint256 oldValue, uint256 newValue) = _userCheckpoints[dst].push(_add, amount); + emit DelegateVotesChanged(dst, oldValue, newValue); + } + } + } + + /** + * @dev Delegates votes from signer to `delegatee` + */ + function delegateBySig( + address delegatee, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + require(block.timestamp <= expiry, "ERC721Votes: signature expired"); + address signer = ECDSA.recover( + _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), + v, + r, + s + ); + require(nonce == _useNonce(signer), "ERC721Votes: invalid nonce"); + _delegate(signer, delegatee); + } + + /** + * @dev Adds two numbers. + */ + function _add(uint256 a, uint256 b) private pure returns (uint256) { + return a + b; + } + + /** + * @dev Subtracts two numbers. + */ + function _subtract(uint256 a, uint256 b) private pure returns (uint256) { + return a - b; + } + + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + Counters.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } + + /** + * @dev Returns an address nonce. + */ + function nonces(address owner) public view virtual returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev Returns DOMAIN_SEPARATOR. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparatorV4(); + } + + function _getDelegatorVotes(address) internal virtual returns(uint256); +} diff --git a/contracts/utils/Voting.sol b/contracts/utils/Voting.sol deleted file mode 100644 index 24d70be1635..00000000000 --- a/contracts/utils/Voting.sol +++ /dev/null @@ -1,220 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "./Checkpoints.sol"; - -/** - * @dev Voting operations. - */ -library Voting { - using Checkpoints for Checkpoints.History; - - struct Votes { - mapping(address => address) _delegation; - mapping(address => Checkpoints.History) _userCheckpoints; - Checkpoints.History _totalCheckpoints; - } - - /** - * @dev Returns total amount of votes for account. - */ - function getVotes(Votes storage self, address account) internal view returns (uint256) { - return self._userCheckpoints[account].latest(); - } - - /** - * @dev Returns total amount of votes at given position. - */ - function getVotesAt( - Votes storage self, - address account, - uint256 timestamp - ) internal view returns (uint256) { - return self._userCheckpoints[account].past(timestamp); - } - - /** - * @dev Get checkpoint for `account` for specific position. - */ - function getTotalAccountVotesAt( - Votes storage self, - address account, - uint32 pos - ) internal view returns (Checkpoints.Checkpoint memory) { - return self._userCheckpoints[account].at(pos); - } - - /** - * @dev Returns total amount of votes. - */ - function getTotalVotes(Votes storage self) internal view returns (uint256) { - return self._totalCheckpoints.latest(); - } - - /** - * @dev Get number of checkpoints for `account` including delegation. - */ - function getTotalAccountVotes(Votes storage self, address account) internal view returns (uint256) { - return self._userCheckpoints[account].length(); - } - - /** - * @dev Returns all votes for timestamp. - */ - function getTotalVotesAt(Votes storage self, uint256 timestamp) internal view returns (uint256) { - return self._totalCheckpoints.past(timestamp); - } - - /** - * @dev Returns account delegation. - */ - function delegates(Votes storage self, address account) internal view returns (address) { - return self._delegation[account]; - } - - /** - * @dev Delegates voting power. - */ - function delegate( - Votes storage self, - address account, - address newDelegation, - uint256 balance - ) internal { - address oldDelegation = delegates(self, account); - self._delegation[account] = newDelegation; - _moveVotingPower(self, oldDelegation, newDelegation, balance, _dummy); - } - - /** - * @dev Delegates voting power. - */ - function delegate( - Votes storage self, - address account, - address newDelegation, - uint256 balance, - function(address, uint256, uint256) hookDelegateVotesChanged - ) internal { - address oldDelegation = delegates(self, account); - self._delegation[account] = newDelegation; - _moveVotingPower(self, oldDelegation, newDelegation, balance, hookDelegateVotesChanged); - } - - /** - * @dev Mints new vote. - */ - function mint( - Votes storage self, - address to, - uint256 amount - ) internal { - self._totalCheckpoints.push(_add, amount); - _moveVotingPower(self, address(0), delegates(self, to), amount, _dummy); - } - - /** - * @dev Mints new vote. - */ - function mint( - Votes storage self, - address to, - uint256 amount, - function(address, uint256, uint256) hookDelegateVotesChanged - ) internal { - self._totalCheckpoints.push(_add, amount); - _moveVotingPower(self, address(0), delegates(self, to), amount, hookDelegateVotesChanged); - } - - /** - * @dev Burns new vote. - */ - function burn( - Votes storage self, - address from, - uint256 amount - ) internal { - self._totalCheckpoints.push(_subtract, amount); - _moveVotingPower(self, delegates(self, from), address(0), amount, _dummy); - } - - /** - * @dev Burns new vote. - */ - function burn( - Votes storage self, - address from, - uint256 amount, - function(address, uint256, uint256) hookDelegateVotesChanged - ) internal { - self._totalCheckpoints.push(_subtract, amount); - _moveVotingPower(self, delegates(self, from), address(0), amount, hookDelegateVotesChanged); - } - - /** - * @dev Transfers voting power. - */ - function transfer( - Votes storage self, - address from, - address to, - uint256 amount - ) internal { - _moveVotingPower(self, delegates(self, from), delegates(self, to), amount, _dummy); - } - - /** - * @dev Transfers voting power. - */ - function transfer( - Votes storage self, - address from, - address to, - uint256 amount, - function(address, uint256, uint256) hookDelegateVotesChanged - ) internal { - _moveVotingPower(self, delegates(self, from), delegates(self, to), amount, hookDelegateVotesChanged); - } - - /** - * @dev Moves voting power. - */ - function _moveVotingPower( - Votes storage self, - address src, - address dst, - uint256 amount, - function(address, uint256, uint256) hookDelegateVotesChanged - ) private { - if (src != dst && amount > 0) { - if (src != address(0)) { - (uint256 oldValue, uint256 newValue) = self._userCheckpoints[src].push(_subtract, amount); - hookDelegateVotesChanged(src, oldValue, newValue); - } - if (dst != address(0)) { - (uint256 oldValue, uint256 newValue) = self._userCheckpoints[dst].push(_add, amount); - hookDelegateVotesChanged(dst, oldValue, newValue); - } - } - } - - /** - * @dev Adds two numbers. - */ - function _add(uint256 a, uint256 b) private pure returns (uint256) { - return a + b; - } - - /** - * @dev Subtracts two numbers. - */ - function _subtract(uint256 a, uint256 b) private pure returns (uint256) { - return a - b; - } - - function _dummy( - address, - uint256, - uint256 - ) private pure {} -} From 176d166571fdfcf4e126da0b08c22c14ef5b641e Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 1 Dec 2021 09:29:10 -0400 Subject: [PATCH 241/300] Add _getDelegatorVotes function for ERC721Votes --- contracts/mocks/ERC721VotesMock.sol | 2 +- .../mocks/{VotingImpl.sol => VotingMock.sol} | 29 +++++++++---------- .../ERC721/extensions/draft-ERC721Votes.sol | 9 +++++- contracts/utils/Votes.sol | 3 ++ 4 files changed, 25 insertions(+), 18 deletions(-) rename contracts/mocks/{VotingImpl.sol => VotingMock.sol} (61%) diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index cd593bb5437..6331d8331bd 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "../token/ERC721/extensions/draft-ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { - constructor(string memory name, string memory symbol) ERC721Votes(name, symbol) {} + constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} function mint(address account, uint256 tokenId) public { _mint(account, tokenId); diff --git a/contracts/mocks/VotingImpl.sol b/contracts/mocks/VotingMock.sol similarity index 61% rename from contracts/mocks/VotingImpl.sol rename to contracts/mocks/VotingMock.sol index 83a569ec31d..2491e933f88 100644 --- a/contracts/mocks/VotingImpl.sol +++ b/contracts/mocks/VotingMock.sol @@ -2,39 +2,36 @@ pragma solidity ^0.8.0; -import "../utils/Voting.sol"; +import "../utils/Votes.sol"; -contract VotingImpl { - using Voting for Voting.Votes; - - Voting.Votes private _votes; +abstract contract VotesMock is Votes { function getVotes(address account) public view returns (uint256) { - return _votes.getVotes(account); + return _getVotes(account); } function getVotesAt(address account, uint256 timestamp) public view returns (uint256) { - return _votes.getVotesAt(account, timestamp); + return _getPastVotes(account, timestamp); } function getTotalAccountVotesAt(address account, uint32 pos) public view returns (Checkpoints.Checkpoint memory) { - return _votes.getTotalAccountVotesAt(account, pos); + return _getTotalAccountVotesAt(account, pos); } function getTotalVotes() public view returns (uint256) { - return _votes.getTotalVotes(); + return _getTotalVotes(); } function getTotalAccountVotes(address account) public view returns (uint256) { - return _votes.getTotalAccountVotes(account); + return _getTotalAccountVotes(account); } function getTotalVotesAt(uint256 timestamp) public view returns (uint256) { - return _votes.getTotalVotesAt(timestamp); + return super.getTotalVotesAt(timestamp); } function delegates(address account) public view returns (address) { - return _votes.delegates(account); + return _delegates(account); } function delegate( @@ -42,15 +39,15 @@ contract VotingImpl { address newDelegation, uint256 balance ) public { - return _votes.delegate(account, newDelegation, balance); + return _delegate(account, newDelegation, balance); } function mint(address to, uint256 amount) public { - return _votes.mint(to, amount); + return _mintVote(to, amount); } function burn(address from, uint256 amount) public { - return _votes.burn(from, amount); + return _burnVote(from, amount); } function transfer( @@ -58,6 +55,6 @@ contract VotingImpl { address to, uint256 amount ) public { - return _votes.transfer(from, to, amount); + return _transferVote(from, to, amount); } } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 30a45e01e09..4e3dc15bfee 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -63,7 +63,7 @@ abstract contract ERC721Votes is ERC721, Votes { * @dev Move voting power when tokens are transferred. * * Emits a {DelegateVotesChanged} event. - */ + */ function _afterTokenTransfer( address from, address to, @@ -73,4 +73,11 @@ abstract contract ERC721Votes is ERC721, Votes { _moveVotingPower(_delegates(from), _delegates(to), 1); } + + /** + * @dev Returns the balance of the delegator account + */ + function _getDelegatorVotes(address delegator) internal virtual override returns(uint256){ + return balanceOf(delegator); + } } diff --git a/contracts/utils/Votes.sol b/contracts/utils/Votes.sol index 2e23b718ded..d857d11cdb9 100644 --- a/contracts/utils/Votes.sol +++ b/contracts/utils/Votes.sol @@ -231,5 +231,8 @@ abstract contract Votes is EIP712 { return _domainSeparatorV4(); } + /** + * @dev Returns the balance of the delegator account + */ function _getDelegatorVotes(address) internal virtual returns(uint256); } From bb32fba4de7ae814ed9c86d0fd2547d2f12bea69 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 1 Dec 2021 16:38:29 -0400 Subject: [PATCH 242/300] Update Voting test and Mock --- contracts/mocks/VotingMock.sol | 22 ++++-------- .../ERC721/extensions/draft-ERC721Votes.sol | 14 ++++++-- contracts/utils/Votes.sol | 35 ++----------------- test/utils/Voting.test.js | 27 +++----------- 4 files changed, 24 insertions(+), 74 deletions(-) diff --git a/contracts/mocks/VotingMock.sol b/contracts/mocks/VotingMock.sol index 2491e933f88..fe829948455 100644 --- a/contracts/mocks/VotingMock.sol +++ b/contracts/mocks/VotingMock.sol @@ -4,7 +4,9 @@ pragma solidity ^0.8.0; import "../utils/Votes.sol"; -abstract contract VotesMock is Votes { +contract VotesMock is Votes { + + constructor(string memory name)EIP712(name, "1") {} function getVotes(address account) public view returns (uint256) { return _getVotes(account); @@ -27,7 +29,7 @@ abstract contract VotesMock is Votes { } function getTotalVotesAt(uint256 timestamp) public view returns (uint256) { - return super.getTotalVotesAt(timestamp); + return getPastTotalSupply(timestamp); } function delegates(address account) public view returns (address) { @@ -42,19 +44,7 @@ abstract contract VotesMock is Votes { return _delegate(account, newDelegation, balance); } - function mint(address to, uint256 amount) public { - return _mintVote(to, amount); - } - - function burn(address from, uint256 amount) public { - return _burnVote(from, amount); - } - - function transfer( - address from, - address to, - uint256 amount - ) public { - return _transferVote(from, to, amount); + function _getDelegatorVotes(address) internal virtual override returns(uint256){ + return 1; } } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 4e3dc15bfee..5fcc7097da9 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -34,6 +34,16 @@ abstract contract ERC721Votes is ERC721, Votes { _delegate(_msgSender(), delegatee); } + /** + * @dev Returns total amount of votes at given position. + */ + function getPastVotes( + address account, + uint256 timestamp + ) external view returns (uint256) { + return _getPastVotes(account, timestamp); + } + /** * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). */ @@ -47,7 +57,7 @@ abstract contract ERC721Votes is ERC721, Votes { function _mint(address account, uint256 tokenId) internal virtual override { super._mint(account, tokenId); - _mintVote(account, 1); + _afterTokenTransfer(address(0), account, tokenId); } /** @@ -56,7 +66,7 @@ abstract contract ERC721Votes is ERC721, Votes { function _burn(uint256 tokenId) internal virtual override { address from = ownerOf(tokenId); super._burn(tokenId); - _burnVote(from, 1); + _afterTokenTransfer(from, address(0), tokenId); } /** diff --git a/contracts/utils/Votes.sol b/contracts/utils/Votes.sol index d857d11cdb9..299f96a2bbc 100644 --- a/contracts/utils/Votes.sol +++ b/contracts/utils/Votes.sol @@ -116,39 +116,6 @@ abstract contract Votes is EIP712 { _moveVotingPower(oldDelegation, newDelegation, balance); } - /** - * @dev Mints new vote. - */ - function _mintVote( - address to, - uint256 amount - ) internal { - _totalCheckpoints.push(_add, amount); - _moveVotingPower(address(0), _delegates(to), amount); - } - - /** - * @dev Burns new vote. - */ - function _burnVote( - address from, - uint256 amount - ) internal { - _totalCheckpoints.push(_subtract, amount); - _moveVotingPower(_delegates(from), address(0), amount); - } - - /** - * @dev Transfers voting power. - */ - function _transferVote( - address from, - address to, - uint256 amount - ) internal { - _moveVotingPower(_delegates(from), _delegates(to), amount); - } - /** * @dev Moves voting power. */ @@ -159,10 +126,12 @@ abstract contract Votes is EIP712 { ) internal { if (src != dst && amount > 0) { if (src != address(0)) { + _totalCheckpoints.push(_subtract, amount); (uint256 oldValue, uint256 newValue) = _userCheckpoints[src].push(_subtract, amount); emit DelegateVotesChanged(src, oldValue, newValue); } if (dst != address(0)) { + _totalCheckpoints.push(_add, amount); (uint256 oldValue, uint256 newValue) = _userCheckpoints[dst].push(_add, amount); emit DelegateVotesChanged(dst, oldValue, newValue); } diff --git a/test/utils/Voting.test.js b/test/utils/Voting.test.js index 06afa2736ba..4a21b6c8450 100644 --- a/test/utils/Voting.test.js +++ b/test/utils/Voting.test.js @@ -2,15 +2,15 @@ const { expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); -const VotingImp = artifacts.require('VotingImpl'); +const Votes = artifacts.require('VotesMock'); contract('Voting', function (accounts) { const [ account1, account2, account3 ] = accounts; beforeEach(async function () { - this.voting = await VotingImp.new(); + this.voting = await Votes.new("MyVote"); }); - it('starts with zero votes', async function () { + it.only('starts with zero votes', async function () { expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('0'); }); @@ -21,14 +21,6 @@ contract('Voting', function (accounts) { this.tx3 = await this.voting.mint(account3, 1); }); - it('mints', async function () { - expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('3'); - - expect(await this.voting.getTotalVotesAt(this.tx1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.voting.getTotalVotesAt(this.tx2.receipt.blockNumber - 1)).to.be.bignumber.equal('1'); - expect(await this.voting.getTotalVotesAt(this.tx3.receipt.blockNumber - 1)).to.be.bignumber.equal('2'); - }); - it('reverts if block number >= current block', async function () { await expectRevert( this.voting.getTotalVotesAt(this.tx3.receipt.blockNumber + 1), @@ -36,18 +28,7 @@ contract('Voting', function (accounts) { ); }); - it('burns', async function () { - await this.voting.burn(account1, 1); - expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('2'); - - await this.voting.burn(account2, 1); - expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('1'); - - await this.voting.burn(account3, 1); - expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('0'); - }); - - it('delegates', async function () { + it.only('delegates', async function () { await this.voting.delegate(account3, account2, 1); expect(await this.voting.delegates(account3)).to.be.equal(account2); From 1d0c1ead0489f2f9feb6854f8aeb11b2173b6a7a Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 2 Dec 2021 07:46:09 -0400 Subject: [PATCH 243/300] Update Votes mock and tests --- contracts/mocks/VotingMock.sol | 4 ++++ test/utils/Voting.test.js | 18 +++++------------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/contracts/mocks/VotingMock.sol b/contracts/mocks/VotingMock.sol index fe829948455..c31e0b04277 100644 --- a/contracts/mocks/VotingMock.sol +++ b/contracts/mocks/VotingMock.sol @@ -47,4 +47,8 @@ contract VotesMock is Votes { function _getDelegatorVotes(address) internal virtual override returns(uint256){ return 1; } + + function giveVotingPower(address account, uint8 amount) external { + _moveVotingPower(address(0), _delegates(account), amount); + } } diff --git a/test/utils/Voting.test.js b/test/utils/Voting.test.js index 4a21b6c8450..d4bb3d36258 100644 --- a/test/utils/Voting.test.js +++ b/test/utils/Voting.test.js @@ -10,15 +10,15 @@ contract('Voting', function (accounts) { this.voting = await Votes.new("MyVote"); }); - it.only('starts with zero votes', async function () { + it('starts with zero votes', async function () { expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('0'); }); describe('move voting power', function () { beforeEach(async function () { - this.tx1 = await this.voting.mint(account1, 1); - this.tx2 = await this.voting.mint(account2, 1); - this.tx3 = await this.voting.mint(account3, 1); + this.tx1 = await this.voting.giveVotingPower(account1, 1); + this.tx2 = await this.voting.giveVotingPower(account2, 1); + this.tx3 = await this.voting.giveVotingPower(account3, 1); }); it('reverts if block number >= current block', async function () { @@ -28,18 +28,10 @@ contract('Voting', function (accounts) { ); }); - it.only('delegates', async function () { + it('delegates', async function () { await this.voting.delegate(account3, account2, 1); expect(await this.voting.delegates(account3)).to.be.equal(account2); }); - - it('transfers', async function () { - await this.voting.delegate(account1, account2, 1); - await this.voting.transfer(account1, account2, 1); - - expect(await this.voting.getTotalAccountVotes(account1)).to.be.bignumber.equal('0'); - expect(await this.voting.getTotalAccountVotes(account2)).to.be.bignumber.equal('2'); - }); }); }); From 3e14830b92eaf1623d2dd6989426036c7e6e17a7 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 2 Dec 2021 08:09:02 -0400 Subject: [PATCH 244/300] Fix functions visibility on Votes contract --- contracts/mocks/VotingMock.sol | 10 +----- .../ERC721/extensions/draft-ERC721Votes.sol | 2 +- contracts/utils/Votes.sol | 8 ++--- .../ERC721/extensions/ERC721Votes.test.js | 36 +++++++++---------- 4 files changed, 24 insertions(+), 32 deletions(-) diff --git a/contracts/mocks/VotingMock.sol b/contracts/mocks/VotingMock.sol index c31e0b04277..c11f434afac 100644 --- a/contracts/mocks/VotingMock.sol +++ b/contracts/mocks/VotingMock.sol @@ -8,10 +8,6 @@ contract VotesMock is Votes { constructor(string memory name)EIP712(name, "1") {} - function getVotes(address account) public view returns (uint256) { - return _getVotes(account); - } - function getVotesAt(address account, uint256 timestamp) public view returns (uint256) { return _getPastVotes(account, timestamp); } @@ -32,10 +28,6 @@ contract VotesMock is Votes { return getPastTotalSupply(timestamp); } - function delegates(address account) public view returns (address) { - return _delegates(account); - } - function delegate( address account, address newDelegation, @@ -49,6 +41,6 @@ contract VotesMock is Votes { } function giveVotingPower(address account, uint8 amount) external { - _moveVotingPower(address(0), _delegates(account), amount); + _moveVotingPower(address(0), delegates(account), amount); } } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 5fcc7097da9..9b92af53da1 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -81,7 +81,7 @@ abstract contract ERC721Votes is ERC721, Votes { ) internal virtual override{ super._afterTokenTransfer(from, to, tokenId); - _moveVotingPower(_delegates(from), _delegates(to), 1); + _moveVotingPower(delegates(from), delegates(to), 1); } /** diff --git a/contracts/utils/Votes.sol b/contracts/utils/Votes.sol index 299f96a2bbc..5845d22e2f8 100644 --- a/contracts/utils/Votes.sol +++ b/contracts/utils/Votes.sol @@ -32,7 +32,7 @@ abstract contract Votes is EIP712 { /** * @dev Returns total amount of votes for account. */ - function _getVotes(address account) internal view returns (uint256) { + function getVotes(address account) public view returns (uint256) { return _userCheckpoints[account].latest(); } @@ -89,14 +89,14 @@ abstract contract Votes is EIP712 { * Emits events {DelegateChanged} and {DelegateVotesChanged}. */ function _delegate(address delegator, address delegatee) internal virtual { - emit DelegateChanged(delegator, _delegates(delegator), delegatee); + emit DelegateChanged(delegator, delegates(delegator), delegatee); _delegate(delegator, delegatee, _getDelegatorVotes(delegator)); } /** * @dev Returns account delegation. */ - function _delegates(address account) internal view returns (address) { + function delegates(address account) public view returns (address) { return _delegation[account]; } @@ -108,7 +108,7 @@ abstract contract Votes is EIP712 { address newDelegation, uint256 balance ) internal { - address oldDelegation = _delegates(account); + address oldDelegation = delegates(account); _delegation[account] = newDelegation; emit DelegateChanged(account, oldDelegation, newDelegation); diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 4dc442c46f6..8fb9e361682 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -425,20 +425,20 @@ contract('ERC721Votes', function (accounts) { }); }); - describe('getPastVotingPower', function () { + 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.getPastVotingPower(5e10), + this.token.getPastTotalSupply(5e10), 'ERC721Votes: block not yet mined', ); }); it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastVotingPower(0)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); }); it('returns the latest block if >= last checkpoint block', async function () { @@ -447,8 +447,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('returns zero if < first checkpoint block', async function () { @@ -457,8 +457,8 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { @@ -478,17 +478,17 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotingPower(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotingPower(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotingPower(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotingPower(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotingPower(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotingPower(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotingPower(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotingPower(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotingPower(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); }); }); From f7a7b5106c153b980dc5cc77afda570c71bf0e5a Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 2 Dec 2021 08:20:52 -0400 Subject: [PATCH 245/300] Run lint --- contracts/mocks/VotingMock.sol | 5 ++--- .../ERC721/extensions/draft-ERC721Votes.sol | 16 +++++--------- contracts/utils/Votes.sol | 22 +++++++++---------- test/utils/Voting.test.js | 2 +- 4 files changed, 19 insertions(+), 26 deletions(-) diff --git a/contracts/mocks/VotingMock.sol b/contracts/mocks/VotingMock.sol index c11f434afac..f7cd962bc89 100644 --- a/contracts/mocks/VotingMock.sol +++ b/contracts/mocks/VotingMock.sol @@ -5,8 +5,7 @@ pragma solidity ^0.8.0; import "../utils/Votes.sol"; contract VotesMock is Votes { - - constructor(string memory name)EIP712(name, "1") {} + constructor(string memory name) EIP712(name, "1") {} function getVotesAt(address account, uint256 timestamp) public view returns (uint256) { return _getPastVotes(account, timestamp); @@ -36,7 +35,7 @@ contract VotesMock is Votes { return _delegate(account, newDelegation, balance); } - function _getDelegatorVotes(address) internal virtual override returns(uint256){ + function _getDelegatorVotes(address) internal virtual override returns (uint256) { return 1; } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 9b92af53da1..724f2fef100 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -26,7 +26,6 @@ import "../../../utils/cryptography/ECDSA.sol"; * _Available since v4.5._ */ abstract contract ERC721Votes is ERC721, Votes { - /** * @dev Delegate votes from the sender to `delegatee`. */ @@ -36,11 +35,8 @@ abstract contract ERC721Votes is ERC721, Votes { /** * @dev Returns total amount of votes at given position. - */ - function getPastVotes( - address account, - uint256 timestamp - ) external view returns (uint256) { + */ + function getPastVotes(address account, uint256 timestamp) external view returns (uint256) { return _getPastVotes(account, timestamp); } @@ -73,12 +69,12 @@ abstract contract ERC721Votes is ERC721, Votes { * @dev Move voting power when tokens are transferred. * * Emits a {DelegateVotesChanged} event. - */ + */ function _afterTokenTransfer( address from, address to, uint256 tokenId - ) internal virtual override{ + ) internal virtual override { super._afterTokenTransfer(from, to, tokenId); _moveVotingPower(delegates(from), delegates(to), 1); @@ -86,8 +82,8 @@ abstract contract ERC721Votes is ERC721, Votes { /** * @dev Returns the balance of the delegator account - */ - function _getDelegatorVotes(address delegator) internal virtual override returns(uint256){ + */ + function _getDelegatorVotes(address delegator) internal virtual override returns (uint256) { return balanceOf(delegator); } } diff --git a/contracts/utils/Votes.sol b/contracts/utils/Votes.sol index 5845d22e2f8..ea3abce8ac6 100644 --- a/contracts/utils/Votes.sol +++ b/contracts/utils/Votes.sol @@ -39,10 +39,7 @@ abstract contract Votes is EIP712 { /** * @dev Returns total amount of votes at given position. */ - function _getPastVotes( - address account, - uint256 timestamp - ) internal view returns (uint256) { + function _getPastVotes(address account, uint256 timestamp) internal view returns (uint256) { return _userCheckpoints[account].past(timestamp); } @@ -62,10 +59,11 @@ abstract contract Votes is EIP712 { /** * @dev Get checkpoint for `account` for specific position. */ - function _getTotalAccountVotesAt( - address account, - uint32 pos - ) internal view returns (Checkpoints.Checkpoint memory) { + function _getTotalAccountVotesAt(address account, uint32 pos) + internal + view + returns (Checkpoints.Checkpoint memory) + { return _userCheckpoints[account].at(pos); } @@ -178,7 +176,7 @@ abstract contract Votes is EIP712 { * @dev "Consume a nonce": return the current value and increment. * * _Available since v4.1._ - */ + */ function _useNonce(address owner) internal virtual returns (uint256 current) { Counters.Counter storage nonce = _nonces[owner]; current = nonce.current(); @@ -187,7 +185,7 @@ abstract contract Votes is EIP712 { /** * @dev Returns an address nonce. - */ + */ function nonces(address owner) public view virtual returns (uint256) { return _nonces[owner].current(); } @@ -202,6 +200,6 @@ abstract contract Votes is EIP712 { /** * @dev Returns the balance of the delegator account - */ - function _getDelegatorVotes(address) internal virtual returns(uint256); + */ + function _getDelegatorVotes(address) internal virtual returns (uint256); } diff --git a/test/utils/Voting.test.js b/test/utils/Voting.test.js index d4bb3d36258..90bce3c6965 100644 --- a/test/utils/Voting.test.js +++ b/test/utils/Voting.test.js @@ -7,7 +7,7 @@ const Votes = artifacts.require('VotesMock'); contract('Voting', function (accounts) { const [ account1, account2, account3 ] = accounts; beforeEach(async function () { - this.voting = await Votes.new("MyVote"); + this.voting = await Votes.new('MyVote'); }); it('starts with zero votes', async function () { From 5704b3be68e1c4e5967a338d60e60841b8e42d21 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 2 Dec 2021 11:36:30 -0400 Subject: [PATCH 246/300] Fix delegation, add test coverage, fix moveVotingPower --- contracts/mocks/ERC721VotesMock.sol | 4 ++ contracts/mocks/VotingMock.sol | 2 +- .../ERC721/extensions/draft-ERC721Votes.sol | 9 ---- contracts/utils/Votes.sol | 25 ++++++---- .../ERC721/extensions/ERC721Votes.test.js | 49 +++++++++++++------ test/utils/Voting.test.js | 7 +++ 6 files changed, 60 insertions(+), 36 deletions(-) diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 6331d8331bd..e5dbf5583b8 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -18,4 +18,8 @@ contract ERC721VotesMock is ERC721Votes { function getChainId() external view returns (uint256) { return block.chainid; } + + function maxSupply() public view returns (uint224) { + return _maxSupply(); + } } diff --git a/contracts/mocks/VotingMock.sol b/contracts/mocks/VotingMock.sol index f7cd962bc89..78f6737dbd7 100644 --- a/contracts/mocks/VotingMock.sol +++ b/contracts/mocks/VotingMock.sol @@ -40,6 +40,6 @@ contract VotesMock is Votes { } function giveVotingPower(address account, uint8 amount) external { - _moveVotingPower(address(0), delegates(account), amount); + _moveVotingPower(address(0), account, amount); } } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 724f2fef100..f88483d53a3 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -26,13 +26,6 @@ import "../../../utils/cryptography/ECDSA.sol"; * _Available since v4.5._ */ abstract contract ERC721Votes is ERC721, Votes { - /** - * @dev Delegate votes from the sender to `delegatee`. - */ - function delegate(address delegatee) public virtual { - _delegate(_msgSender(), delegatee); - } - /** * @dev Returns total amount of votes at given position. */ @@ -52,7 +45,6 @@ abstract contract ERC721Votes is ERC721, Votes { */ function _mint(address account, uint256 tokenId) internal virtual override { super._mint(account, tokenId); - _afterTokenTransfer(address(0), account, tokenId); } @@ -76,7 +68,6 @@ abstract contract ERC721Votes is ERC721, Votes { uint256 tokenId ) internal virtual override { super._afterTokenTransfer(from, to, tokenId); - _moveVotingPower(delegates(from), delegates(to), 1); } diff --git a/contracts/utils/Votes.sol b/contracts/utils/Votes.sol index ea3abce8ac6..ead33e6e0d5 100644 --- a/contracts/utils/Votes.sol +++ b/contracts/utils/Votes.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import "./Context.sol"; import "./Counters.sol"; import "./Checkpoints.sol"; import "./cryptography/draft-EIP712.sol"; @@ -8,7 +9,7 @@ import "./cryptography/draft-EIP712.sol"; /** * @dev Voting operations. */ -abstract contract Votes is EIP712 { +abstract contract Votes is EIP712, Context { using Checkpoints for Checkpoints.History; using Counters for Counters.Counter; @@ -82,12 +83,10 @@ abstract contract Votes is EIP712 { } /** - * @dev Change delegation for `delegator` to `delegatee`. - * - * Emits events {DelegateChanged} and {DelegateVotesChanged}. + * @dev Delegate votes from the sender to `delegatee`. */ - function _delegate(address delegator, address delegatee) internal virtual { - emit DelegateChanged(delegator, delegates(delegator), delegatee); + function delegate(address delegatee) public virtual { + address delegator = _msgSender(); _delegate(delegator, delegatee, _getDelegatorVotes(delegator)); } @@ -99,7 +98,9 @@ abstract contract Votes is EIP712 { } /** - * @dev Delegates voting power. + * @dev Change delegation for `delegator` to `delegatee`. + * + * Emits events {DelegateChanged} and {DelegateVotesChanged}. */ function _delegate( address account, @@ -123,13 +124,17 @@ abstract contract Votes is EIP712 { uint256 amount ) internal { if (src != dst && amount > 0) { - if (src != address(0)) { + if (dst == address(0) && src != address(0)) { _totalCheckpoints.push(_subtract, amount); + } else if (src == address(0) && dst != address(0)) { + _totalCheckpoints.push(_add, amount); + } + + if (src != address(0)) { (uint256 oldValue, uint256 newValue) = _userCheckpoints[src].push(_subtract, amount); emit DelegateVotesChanged(src, oldValue, newValue); } if (dst != address(0)) { - _totalCheckpoints.push(_add, amount); (uint256 oldValue, uint256 newValue) = _userCheckpoints[dst].push(_add, amount); emit DelegateVotesChanged(dst, oldValue, newValue); } @@ -155,7 +160,7 @@ abstract contract Votes is EIP712 { s ); require(nonce == _useNonce(signer), "ERC721Votes: invalid nonce"); - _delegate(signer, delegatee); + _delegate(signer, delegatee, _getDelegatorVotes(signer)); } /** diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 8fb9e361682..937a4d8305c 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -84,6 +84,10 @@ contract('ERC721Votes', function (accounts) { ); }); + it('returns max supply', async function () { + expect(await this.token.maxSupply()).to.be.bignumber; + }); + describe('set delegation', function () { describe('call', function () { it('delegation with tokens', async function () { @@ -427,6 +431,7 @@ contract('ERC721Votes', function (accounts) { describe('getPastTotalSupply', function () { beforeEach(async function () { + t1 = await this.token.mint(holder, NFT0); await this.token.delegate(holder, { from: holder }); }); @@ -442,23 +447,35 @@ contract('ERC721Votes', function (accounts) { }); it('returns the latest block if >= last checkpoint block', async function () { - t1 = await this.token.mint(holder, NFT0); await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); }); + it('returns the same total supply on transfers', async function () { + await this.token.delegate(holder, { from: holder }); + await this.token.delegate(recipient, { from: recipient }); + + const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(receipt.blockNumber - 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + }); + it('returns zero if < first checkpoint block', async function () { await time.advanceBlock(); - const t1 = await this.token.mint(holder, NFT0); + const t2 = await this.token.mint(holder, NFT1); await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber - 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { @@ -478,17 +495,17 @@ contract('ERC721Votes', function (accounts) { await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); }); }); }); diff --git a/test/utils/Voting.test.js b/test/utils/Voting.test.js index 90bce3c6965..d170c353ae6 100644 --- a/test/utils/Voting.test.js +++ b/test/utils/Voting.test.js @@ -33,5 +33,12 @@ contract('Voting', function (accounts) { expect(await this.voting.delegates(account3)).to.be.equal(account2); }); + + it('returns amount of votes for account', async function () { + const Checkpoint = await this.voting.getTotalAccountVotesAt(account1, 0); + + expect(Checkpoint[1]).to.be.bignumber.equal('1'); + expect(await this.voting.getTotalAccountVotes(account1)).to.be.bignumber.equal('1'); + }); }); }); From c37502fccb4479537658396bbc3c0a4b2dccc3b9 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 2 Dec 2021 14:29:41 -0400 Subject: [PATCH 247/300] Change inheritance order --- contracts/utils/Votes.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/Votes.sol b/contracts/utils/Votes.sol index ead33e6e0d5..702a3e19a08 100644 --- a/contracts/utils/Votes.sol +++ b/contracts/utils/Votes.sol @@ -9,7 +9,7 @@ import "./cryptography/draft-EIP712.sol"; /** * @dev Voting operations. */ -abstract contract Votes is EIP712, Context { +abstract contract Votes is Context, EIP712 { using Checkpoints for Checkpoints.History; using Counters for Counters.Counter; From 9ea08bebecc1593fd534b9595224450aaffd16b1 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 2 Dec 2021 15:26:48 -0400 Subject: [PATCH 248/300] Update documentation contract --- docs/modules/ROOT/pages/governance.adoc | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 8986ff2e069..4e9147e886f 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -20,7 +20,7 @@ The ERC20 extension to keep track of votes and vote delegation is one such case. === ERC721Votes -The ERC721 extension to keep track of votes and vote delegation is one such case. +The ERC721 extension to keep track of votes and vote delegation is one such case. === Governor & GovernorCompatibilityBravo @@ -129,19 +129,20 @@ If your project requires The voting power of each account in our governance setu // SPDX-License-Identifier: MIT pragma solidity ^0.8.2; -import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/draft-ERC721Votes.sol"; +import "openzeppelin-solidity/contracts/token/ERC721/ERC721.sol"; +import "openzeppelin-solidity/contracts/token/ERC721/extensions/draft-ERC721Votes.sol"; contract MyToken is ERC721Votes { - constructor() ERC721Votes("MyToken", "MTK"){} + constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} // The functions below are overrides required by Solidity. function _afterTokenTransfer( address from, - address to + address to, + uint256 tokenId ) internal override(ERC721Votes) { - super._afterTokenTransfer(from, to); + super._afterTokenTransfer(from, to, tokenId); } function _mint(address to, uint256 tokenId) internal override(ERC721Votes) { @@ -331,7 +332,7 @@ This will create a new proposal, with a proposal id that is obtained by hashing === Cast a Vote -Once a proposal is active, stakeholders can cast their vote. This is done through a function in the Governor contract that users can invoke directly from a governance UI such as Tally. +Once a proposal is active, stakeholders can cast their vote. This is done through a function in the Governor contract that users can invoke directly from a governance UI such as Tally. image::tally-vote.png[Voting in Tally] From 6bcff2b1acadd3bd147e0f657790f713645033eb Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 07:11:50 -0400 Subject: [PATCH 249/300] Update CHANGELOG.md Co-authored-by: Francisco Giordano --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c13adfe6b5..110a644ad3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ## Unreleased -* `Voting`: Create library to be use for ERC721 and ERC1155 voting ([#2944](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2944)) +* `Voting`: Added a library for vote tracking with delegation. ([#2944](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2944)) +* `ERC721Votes`: Added an extension of ERC721 enabled with vote tracking and delegation. ([#2944](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2944)) * `GovernorTimelockControl`: improve the `state()` function to have it reflect cases where a proposal has been canceled directly on the timelock. ([#2977](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2977)) * `Math`: add a `abs(int256)` method that returns the unsigned absolute value of a signed value. ([#2984](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2984)) * Preset contracts are now deprecated in favor of [Contracts Wizard](https://wizard.openzeppelin.com). ([#2986](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2986)) From 73bb657474f6c22442a1dbed2f899aabe60a97b1 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 07:14:09 -0400 Subject: [PATCH 250/300] Update checkpoints storage variable names --- contracts/utils/Checkpoints.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index dacd0649e4f..56ecfa53c33 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -9,8 +9,8 @@ import "./math/SafeCast.sol"; */ library Checkpoints { struct Checkpoint { - uint32 index; - uint224 value; + uint32 _blockNumber; + uint224 _value; } struct History { @@ -36,7 +36,7 @@ library Checkpoints { */ function latest(History storage self) internal view returns (uint256) { uint256 pos = length(self); - return pos == 0 ? 0 : at(self, pos - 1).value; + return pos == 0 ? 0 : at(self, pos - 1)._value; } /** @@ -49,13 +49,13 @@ library Checkpoints { uint256 low = 0; while (low < high) { uint256 mid = Math.average(low, high); - if (at(self, mid).index > index) { + if (at(self, mid)._blockNumber > index) { high = mid; } else { low = mid + 1; } } - return high == 0 ? 0 : at(self, high - 1).value; + return high == 0 ? 0 : at(self, high - 1)._value; } /** @@ -64,8 +64,8 @@ library Checkpoints { function push(History storage self, uint256 value) internal returns (uint256, uint256) { uint256 pos = length(self); uint256 old = latest(self); - if (pos > 0 && self._checkpoints[pos - 1].index == block.number) { - self._checkpoints[pos - 1].value = SafeCast.toUint224(value); + if (pos > 0 && self._checkpoints[pos - 1]._blockNumber == block.number) { + self._checkpoints[pos - 1]._value = SafeCast.toUint224(value); } else { self._checkpoints.push( Checkpoint({index: SafeCast.toUint32(block.number), value: SafeCast.toUint224(value)}) From 3ffdb76d99f119525fead4162b4dc977739da06e Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 08:36:41 -0400 Subject: [PATCH 251/300] Update docs/modules/ROOT/pages/governance.adoc Co-authored-by: Francisco Giordano --- docs/modules/ROOT/pages/governance.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 4e9147e886f..abd3277ede9 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -133,7 +133,7 @@ import "openzeppelin-solidity/contracts/token/ERC721/ERC721.sol"; import "openzeppelin-solidity/contracts/token/ERC721/extensions/draft-ERC721Votes.sol"; contract MyToken is ERC721Votes { - constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} + constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} // The functions below are overrides required by Solidity. From b6cbe1d5a19fc838ecabefa69b862a36b617e896 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 08:37:33 -0400 Subject: [PATCH 252/300] Update variable names, Remove index based functions, update documentation --- contracts/mocks/CheckpointsImpl.sol | 8 +- contracts/mocks/VotingMock.sol | 8 -- .../ERC721/extensions/draft-ERC721Votes.sol | 6 - contracts/utils/Checkpoints.sol | 25 ++--- contracts/utils/Votes.sol | 105 +++++++++--------- docs/modules/ROOT/pages/governance.adoc | 4 +- 6 files changed, 66 insertions(+), 90 deletions(-) diff --git a/contracts/mocks/CheckpointsImpl.sol b/contracts/mocks/CheckpointsImpl.sol index ee409a45055..5489cf7a9ff 100644 --- a/contracts/mocks/CheckpointsImpl.sol +++ b/contracts/mocks/CheckpointsImpl.sol @@ -13,16 +13,12 @@ contract CheckpointsImpl { return _totalCheckpoints.length(); } - function at(uint256 pos) public view returns (Checkpoints.Checkpoint memory) { - return _totalCheckpoints.at(pos); - } - function latest() public view returns (uint256) { return _totalCheckpoints.latest(); } - function past(uint256 index) public view returns (uint256) { - return _totalCheckpoints.past(index); + function past(uint256 blockNumber) public view returns (uint256) { + return _totalCheckpoints.getAtBlock(blockNumber); } function push(uint256 value) public returns (uint256, uint256) { diff --git a/contracts/mocks/VotingMock.sol b/contracts/mocks/VotingMock.sol index 78f6737dbd7..e7c2bd7a4eb 100644 --- a/contracts/mocks/VotingMock.sol +++ b/contracts/mocks/VotingMock.sol @@ -7,14 +7,6 @@ import "../utils/Votes.sol"; contract VotesMock is Votes { constructor(string memory name) EIP712(name, "1") {} - function getVotesAt(address account, uint256 timestamp) public view returns (uint256) { - return _getPastVotes(account, timestamp); - } - - function getTotalAccountVotesAt(address account, uint32 pos) public view returns (Checkpoints.Checkpoint memory) { - return _getTotalAccountVotesAt(account, pos); - } - function getTotalVotes() public view returns (uint256) { return _getTotalVotes(); } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index f88483d53a3..299ccf4a4ac 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -26,12 +26,6 @@ import "../../../utils/cryptography/ECDSA.sol"; * _Available since v4.5._ */ abstract contract ERC721Votes is ERC721, Votes { - /** - * @dev Returns total amount of votes at given position. - */ - function getPastVotes(address account, uint256 timestamp) external view returns (uint256) { - return _getPastVotes(account, timestamp); - } /** * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index 56ecfa53c33..cc1bc3d314d 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -24,51 +24,44 @@ library Checkpoints { return self._checkpoints.length; } - /** - * @dev Returns checkpoints at given position. - */ - function at(History storage self, uint256 pos) internal view returns (Checkpoint memory) { - return self._checkpoints[pos]; - } - /** * @dev Returns total amount of checkpoints. */ function latest(History storage self) internal view returns (uint256) { - uint256 pos = length(self); - return pos == 0 ? 0 : at(self, pos - 1)._value; + uint256 pos = self._checkpoints.length; + return pos == 0 ? 0 : self._checkpoints[pos - 1]._value; } /** * @dev Returns checkpoints at given block number. */ - function past(History storage self, uint256 index) internal view returns (uint256) { - require(index < block.number, "block not yet mined"); + function getAtBlock(History storage self, uint256 blockNumber) internal view returns (uint256) { + require(blockNumber < block.number, "block not yet mined"); - uint256 high = length(self); + uint256 high = self._checkpoints.length; uint256 low = 0; while (low < high) { uint256 mid = Math.average(low, high); - if (at(self, mid)._blockNumber > index) { + if (self._checkpoints[mid]._blockNumber > blockNumber) { high = mid; } else { low = mid + 1; } } - return high == 0 ? 0 : at(self, high - 1)._value; + return high == 0 ? 0 : self._checkpoints[high - 1]._value; } /** * @dev Creates checkpoint */ function push(History storage self, uint256 value) internal returns (uint256, uint256) { - uint256 pos = length(self); + uint256 pos = self._checkpoints.length; uint256 old = latest(self); if (pos > 0 && self._checkpoints[pos - 1]._blockNumber == block.number) { self._checkpoints[pos - 1]._value = SafeCast.toUint224(value); } else { self._checkpoints.push( - Checkpoint({index: SafeCast.toUint32(block.number), value: SafeCast.toUint224(value)}) + Checkpoint({_blockNumber: SafeCast.toUint32(block.number), _value: SafeCast.toUint224(value)}) ); } return (old, value); diff --git a/contracts/utils/Votes.sol b/contracts/utils/Votes.sol index 702a3e19a08..7d434790004 100644 --- a/contracts/utils/Votes.sol +++ b/contracts/utils/Votes.sol @@ -8,6 +8,18 @@ import "./cryptography/draft-EIP712.sol"; /** * @dev Voting operations. + * + * 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 {getVotes}. + * + * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it + * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. + * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this + * will significantly increase the base gas cost of transfers. + * + * When using this module, the derived contract must implement {_getDelegatorVotes}, and can use {_moveVotingPower} + * when a delegator's voting power is changed. */ abstract contract Votes is Context, EIP712 { using Checkpoints for Checkpoints.History; @@ -15,10 +27,10 @@ abstract contract Votes is Context, EIP712 { bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); - mapping(address => address) _delegation; - mapping(address => Checkpoints.History) _userCheckpoints; + mapping(address => address) private _delegation; + mapping(address => Checkpoints.History) private _userCheckpoints; mapping(address => Counters.Counter) private _nonces; - Checkpoints.History _totalCheckpoints; + Checkpoints.History private _totalCheckpoints; /** * @dev Emitted when an account changes their delegate. @@ -33,15 +45,15 @@ abstract contract Votes is Context, EIP712 { /** * @dev Returns total amount of votes for account. */ - function getVotes(address account) public view returns (uint256) { + function getVotes(address account) public view virtual returns (uint256) { return _userCheckpoints[account].latest(); } /** - * @dev Returns total amount of votes at given position. + * @dev Returns total amount of votes at given blockNumber. */ - function _getPastVotes(address account, uint256 timestamp) internal view returns (uint256) { - return _userCheckpoints[account].past(timestamp); + function getPastVotes(address account, uint256 blockNumber) public view virtual returns (uint256) { + return _userCheckpoints[account].getAtBlock(blockNumber); } /** @@ -52,33 +64,22 @@ abstract contract Votes is Context, EIP712 { * * - `blockNumber` must have been already mined */ - function getPastTotalSupply(uint256 blockNumber) public view returns (uint256) { + function getPastTotalSupply(uint256 blockNumber) public view virtual returns (uint256) { require(blockNumber < block.number, "ERC721Votes: block not yet mined"); - return _totalCheckpoints.past(blockNumber); - } - - /** - * @dev Get checkpoint for `account` for specific position. - */ - function _getTotalAccountVotesAt(address account, uint32 pos) - internal - view - returns (Checkpoints.Checkpoint memory) - { - return _userCheckpoints[account].at(pos); + return _totalCheckpoints.getAtBlock(blockNumber); } /** * @dev Returns total amount of votes. */ - function _getTotalVotes() internal view returns (uint256) { + function _getTotalVotes() internal view virtual returns (uint256) { return _totalCheckpoints.latest(); } /** * @dev Get number of checkpoints for `account` including delegation. */ - function _getTotalAccountVotes(address account) internal view returns (uint256) { + function _getTotalAccountVotes(address account) internal view virtual returns (uint256) { return _userCheckpoints[account].length(); } @@ -93,7 +94,7 @@ abstract contract Votes is Context, EIP712 { /** * @dev Returns account delegation. */ - function delegates(address account) public view returns (address) { + function delegates(address account) public view virtual returns (address) { return _delegation[account]; } @@ -106,7 +107,7 @@ abstract contract Votes is Context, EIP712 { address account, address newDelegation, uint256 balance - ) internal { + ) internal virtual{ address oldDelegation = delegates(account); _delegation[account] = newDelegation; @@ -116,34 +117,8 @@ abstract contract Votes is Context, EIP712 { } /** - * @dev Moves voting power. - */ - function _moveVotingPower( - address src, - address dst, - uint256 amount - ) internal { - if (src != dst && amount > 0) { - if (dst == address(0) && src != address(0)) { - _totalCheckpoints.push(_subtract, amount); - } else if (src == address(0) && dst != address(0)) { - _totalCheckpoints.push(_add, amount); - } - - if (src != address(0)) { - (uint256 oldValue, uint256 newValue) = _userCheckpoints[src].push(_subtract, amount); - emit DelegateVotesChanged(src, oldValue, newValue); - } - if (dst != address(0)) { - (uint256 oldValue, uint256 newValue) = _userCheckpoints[dst].push(_add, amount); - emit DelegateVotesChanged(dst, oldValue, newValue); - } - } - } - - /** - * @dev Delegates votes from signer to `delegatee` - */ + * @dev Delegates votes from signer to `delegatee` + */ function delegateBySig( address delegatee, uint256 nonce, @@ -163,6 +138,32 @@ abstract contract Votes is Context, EIP712 { _delegate(signer, delegatee, _getDelegatorVotes(signer)); } + /** + * @dev Moves voting power. + */ + function _moveVotingPower( + address from, + address to, + uint256 amount + ) internal virtual{ + if (from != to && amount > 0) { + if (to == address(0) && from != address(0)) { + _totalCheckpoints.push(_subtract, amount); + } else if (from == address(0) && to != address(0)) { + _totalCheckpoints.push(_add, amount); + } + + if (from != address(0)) { + (uint256 oldValue, uint256 newValue) = _userCheckpoints[from].push(_subtract, amount); + emit DelegateVotesChanged(from, oldValue, newValue); + } + if (to != address(0)) { + (uint256 oldValue, uint256 newValue) = _userCheckpoints[to].push(_add, amount); + emit DelegateVotesChanged(to, oldValue, newValue); + } + } + } + /** * @dev Adds two numbers. */ diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 4e9147e886f..63d821d8581 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -129,8 +129,8 @@ If your project requires The voting power of each account in our governance setu // SPDX-License-Identifier: MIT pragma solidity ^0.8.2; -import "openzeppelin-solidity/contracts/token/ERC721/ERC721.sol"; -import "openzeppelin-solidity/contracts/token/ERC721/extensions/draft-ERC721Votes.sol"; +import "openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "openzeppelin/contracts/token/ERC721/extensions/draft-ERC721Votes.sol"; contract MyToken is ERC721Votes { constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} From f054441f5f9b4d470bcc385a549cebebda94c04e Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 08:56:34 -0400 Subject: [PATCH 253/300] Update delegate arguments, rename variables, add function call on base ERC712 mint and burn calls --- contracts/mocks/ERC721VotesMock.sol | 3 --- contracts/token/ERC721/ERC721.sol | 3 +++ .../ERC721/extensions/draft-ERC721Votes.sol | 26 +------------------ contracts/utils/Votes.sol | 25 +++++++++--------- 4 files changed, 16 insertions(+), 41 deletions(-) diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index e5dbf5583b8..4ad6a3a8bed 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -19,7 +19,4 @@ contract ERC721VotesMock is ERC721Votes { return block.chainid; } - function maxSupply() public view returns (uint224) { - return _maxSupply(); - } } diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 191e83af804..db1bda17bf5 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -287,6 +287,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { _owners[tokenId] = to; emit Transfer(address(0), to, tokenId); + _afterTokenTransfer(address(0), to, tokenId); } /** @@ -311,6 +312,8 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { delete _owners[tokenId]; emit Transfer(owner, address(0), tokenId); + + _afterTokenTransfer(owner, address(0), tokenId); } /** diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 299ccf4a4ac..1045a2128f4 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -27,30 +27,6 @@ import "../../../utils/cryptography/ECDSA.sol"; */ abstract contract ERC721Votes is ERC721, Votes { - /** - * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). - */ - function _maxSupply() internal view virtual returns (uint224) { - return type(uint224).max; - } - - /** - * @dev Snapshots the totalSupply after it has been increased. - */ - function _mint(address account, uint256 tokenId) internal virtual override { - super._mint(account, tokenId); - _afterTokenTransfer(address(0), account, tokenId); - } - - /** - * @dev Snapshots the totalSupply after it has been decreased. - */ - function _burn(uint256 tokenId) internal virtual override { - address from = ownerOf(tokenId); - super._burn(tokenId); - _afterTokenTransfer(from, address(0), tokenId); - } - /** * @dev Move voting power when tokens are transferred. * @@ -68,7 +44,7 @@ abstract contract ERC721Votes is ERC721, Votes { /** * @dev Returns the balance of the delegator account */ - function _getDelegatorVotes(address delegator) internal virtual override returns (uint256) { + function _getDelegatorVotingPower(address delegator) internal virtual override returns (uint256) { return balanceOf(delegator); } } diff --git a/contracts/utils/Votes.sol b/contracts/utils/Votes.sol index 7d434790004..5fabbe6970a 100644 --- a/contracts/utils/Votes.sol +++ b/contracts/utils/Votes.sol @@ -18,7 +18,7 @@ import "./cryptography/draft-EIP712.sol"; * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this * will significantly increase the base gas cost of transfers. * - * When using this module, the derived contract must implement {_getDelegatorVotes}, and can use {_moveVotingPower} + * When using this module, the derived contract must implement {_getDelegatorVotingPower}, and can use {_moveVotingPower} * when a delegator's voting power is changed. */ abstract contract Votes is Context, EIP712 { @@ -27,7 +27,7 @@ abstract contract Votes is Context, EIP712 { bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); - mapping(address => address) private _delegation; + mapping(address => address) private _delegateCheckpoints; mapping(address => Checkpoints.History) private _userCheckpoints; mapping(address => Counters.Counter) private _nonces; Checkpoints.History private _totalCheckpoints; @@ -88,14 +88,14 @@ abstract contract Votes is Context, EIP712 { */ function delegate(address delegatee) public virtual { address delegator = _msgSender(); - _delegate(delegator, delegatee, _getDelegatorVotes(delegator)); + _delegate(delegator, delegatee); } /** * @dev Returns account delegation. */ function delegates(address account) public view virtual returns (address) { - return _delegation[account]; + return _delegateCheckpoints[account]; } /** @@ -104,16 +104,15 @@ abstract contract Votes is Context, EIP712 { * Emits events {DelegateChanged} and {DelegateVotesChanged}. */ function _delegate( - address account, - address newDelegation, - uint256 balance + address delegator, + address newDelegation ) internal virtual{ - address oldDelegation = delegates(account); - _delegation[account] = newDelegation; + address oldDelegation = delegates(delegator); + _delegateCheckpoints[delegator] = newDelegation; - emit DelegateChanged(account, oldDelegation, newDelegation); + emit DelegateChanged(delegator, oldDelegation, newDelegation); - _moveVotingPower(oldDelegation, newDelegation, balance); + _moveVotingPower(oldDelegation, newDelegation, _getDelegatorVotingPower(delegator)); } /** @@ -135,7 +134,7 @@ abstract contract Votes is Context, EIP712 { s ); require(nonce == _useNonce(signer), "ERC721Votes: invalid nonce"); - _delegate(signer, delegatee, _getDelegatorVotes(signer)); + _delegate(signer, delegatee); } /** @@ -207,5 +206,5 @@ abstract contract Votes is Context, EIP712 { /** * @dev Returns the balance of the delegator account */ - function _getDelegatorVotes(address) internal virtual returns (uint256); + function _getDelegatorVotingPower(address) internal virtual returns (uint256); } From 0c8b601daeb1fed2dff3eaf8483adf122672b1ae Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 09:01:24 -0400 Subject: [PATCH 254/300] Update _moveVotingPower --- contracts/utils/Votes.sol | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/contracts/utils/Votes.sol b/contracts/utils/Votes.sol index 5fabbe6970a..c0c64d8ed77 100644 --- a/contracts/utils/Votes.sol +++ b/contracts/utils/Votes.sol @@ -146,17 +146,15 @@ abstract contract Votes is Context, EIP712 { uint256 amount ) internal virtual{ if (from != to && amount > 0) { - if (to == address(0) && from != address(0)) { - _totalCheckpoints.push(_subtract, amount); - } else if (from == address(0) && to != address(0)) { + if (from == address(0)) { _totalCheckpoints.push(_add, amount); - } - - if (from != address(0)) { + } else { (uint256 oldValue, uint256 newValue) = _userCheckpoints[from].push(_subtract, amount); emit DelegateVotesChanged(from, oldValue, newValue); } - if (to != address(0)) { + if (to == address(0)) { + _totalCheckpoints.push(_subtract, amount); + } else { (uint256 oldValue, uint256 newValue) = _userCheckpoints[to].push(_add, amount); emit DelegateVotesChanged(to, oldValue, newValue); } From 7de147c49d77379bcd80eccb092119dec50a44c1 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 09:20:11 -0400 Subject: [PATCH 255/300] Update tests --- contracts/mocks/CheckpointsImpl.sol | 2 +- contracts/mocks/VotingMock.sol | 7 +++---- test/token/ERC721/extensions/ERC721Votes.test.js | 4 ---- test/utils/Checkpoints.test.js | 16 +++++----------- test/utils/Voting.test.js | 5 +---- 5 files changed, 10 insertions(+), 24 deletions(-) diff --git a/contracts/mocks/CheckpointsImpl.sol b/contracts/mocks/CheckpointsImpl.sol index 5489cf7a9ff..0c772c8e4d1 100644 --- a/contracts/mocks/CheckpointsImpl.sol +++ b/contracts/mocks/CheckpointsImpl.sol @@ -17,7 +17,7 @@ contract CheckpointsImpl { return _totalCheckpoints.latest(); } - function past(uint256 blockNumber) public view returns (uint256) { + function getAtBlock(uint256 blockNumber) public view returns (uint256) { return _totalCheckpoints.getAtBlock(blockNumber); } diff --git a/contracts/mocks/VotingMock.sol b/contracts/mocks/VotingMock.sol index e7c2bd7a4eb..c9f24bdbaad 100644 --- a/contracts/mocks/VotingMock.sol +++ b/contracts/mocks/VotingMock.sol @@ -21,13 +21,12 @@ contract VotesMock is Votes { function delegate( address account, - address newDelegation, - uint256 balance + address newDelegation ) public { - return _delegate(account, newDelegation, balance); + return _delegate(account, newDelegation); } - function _getDelegatorVotes(address) internal virtual override returns (uint256) { + function _getDelegatorVotingPower(address) internal virtual override returns (uint256) { return 1; } diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 937a4d8305c..917c08f683c 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -84,10 +84,6 @@ contract('ERC721Votes', function (accounts) { ); }); - it('returns max supply', async function () { - expect(await this.token.maxSupply()).to.be.bignumber; - }); - describe('set delegation', function () { describe('call', function () { it('delegation with tokens', async function () { diff --git a/test/utils/Checkpoints.test.js b/test/utils/Checkpoints.test.js index 33ba473946c..fa087a359dc 100644 --- a/test/utils/Checkpoints.test.js +++ b/test/utils/Checkpoints.test.js @@ -16,25 +16,19 @@ contract('Checkpoints', function (accounts) { expect(await this.checkpoint.length()).to.be.bignumber.equal('3'); }); - it('calls at', async function () { - expect((await this.checkpoint.at(0))[1]).to.be.bignumber.equal('1'); - expect((await this.checkpoint.at(1))[1]).to.be.bignumber.equal('2'); - expect((await this.checkpoint.at(2))[1]).to.be.bignumber.equal('3'); - }); - it('calls latest', async function () { expect(await this.checkpoint.latest()).to.be.bignumber.equal('3'); }); - it('calls past', async function () { - expect(await this.checkpoint.past(this.tx1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.checkpoint.past(this.tx2.receipt.blockNumber - 1)).to.be.bignumber.equal('1'); - expect(await this.checkpoint.past(this.tx3.receipt.blockNumber - 1)).to.be.bignumber.equal('2'); + it('calls getAtBlock', async function () { + expect(await this.checkpoint.getAtBlock(this.tx1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.checkpoint.getAtBlock(this.tx2.receipt.blockNumber - 1)).to.be.bignumber.equal('1'); + expect(await this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber - 1)).to.be.bignumber.equal('2'); }); it('reverts if block number >= current block', async function () { await expectRevert( - this.checkpoint.past(this.tx3.receipt.blockNumber + 1), + this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber + 1), 'block not yet mined', ); }); diff --git a/test/utils/Voting.test.js b/test/utils/Voting.test.js index d170c353ae6..509ed71c5a0 100644 --- a/test/utils/Voting.test.js +++ b/test/utils/Voting.test.js @@ -29,15 +29,12 @@ contract('Voting', function (accounts) { }); it('delegates', async function () { - await this.voting.delegate(account3, account2, 1); + await this.voting.delegate(account3, account2); expect(await this.voting.delegates(account3)).to.be.equal(account2); }); it('returns amount of votes for account', async function () { - const Checkpoint = await this.voting.getTotalAccountVotesAt(account1, 0); - - expect(Checkpoint[1]).to.be.bignumber.equal('1'); expect(await this.voting.getTotalAccountVotes(account1)).to.be.bignumber.equal('1'); }); }); From d852c08dba9959d4e89ab992dc5cc3ca1069f5ff Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 09:29:05 -0400 Subject: [PATCH 256/300] Run lint --- contracts/mocks/ERC721VotesMock.sol | 1 - contracts/mocks/VotingMock.sol | 5 +---- .../token/ERC721/extensions/draft-ERC721Votes.sol | 1 - contracts/utils/Votes.sol | 11 ++++------- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 4ad6a3a8bed..6331d8331bd 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -18,5 +18,4 @@ contract ERC721VotesMock is ERC721Votes { function getChainId() external view returns (uint256) { return block.chainid; } - } diff --git a/contracts/mocks/VotingMock.sol b/contracts/mocks/VotingMock.sol index c9f24bdbaad..bdab57be7cc 100644 --- a/contracts/mocks/VotingMock.sol +++ b/contracts/mocks/VotingMock.sol @@ -19,10 +19,7 @@ contract VotesMock is Votes { return getPastTotalSupply(timestamp); } - function delegate( - address account, - address newDelegation - ) public { + function delegate(address account, address newDelegation) public { return _delegate(account, newDelegation); } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 1045a2128f4..3f31e44bc03 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -26,7 +26,6 @@ import "../../../utils/cryptography/ECDSA.sol"; * _Available since v4.5._ */ abstract contract ERC721Votes is ERC721, Votes { - /** * @dev Move voting power when tokens are transferred. * diff --git a/contracts/utils/Votes.sol b/contracts/utils/Votes.sol index c0c64d8ed77..635193ca37d 100644 --- a/contracts/utils/Votes.sol +++ b/contracts/utils/Votes.sol @@ -103,10 +103,7 @@ abstract contract Votes is Context, EIP712 { * * Emits events {DelegateChanged} and {DelegateVotesChanged}. */ - function _delegate( - address delegator, - address newDelegation - ) internal virtual{ + function _delegate(address delegator, address newDelegation) internal virtual { address oldDelegation = delegates(delegator); _delegateCheckpoints[delegator] = newDelegation; @@ -116,8 +113,8 @@ abstract contract Votes is Context, EIP712 { } /** - * @dev Delegates votes from signer to `delegatee` - */ + * @dev Delegates votes from signer to `delegatee` + */ function delegateBySig( address delegatee, uint256 nonce, @@ -144,7 +141,7 @@ abstract contract Votes is Context, EIP712 { address from, address to, uint256 amount - ) internal virtual{ + ) internal virtual { if (from != to && amount > 0) { if (from == address(0)) { _totalCheckpoints.push(_add, amount); From 5efd9a20238914997fdae291d204f29676d3d123 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 12:47:34 -0400 Subject: [PATCH 257/300] Add IVote interface an update tests --- .../governance/extensions/GovernorVotes.sol | 8 +-- .../extensions/GovernorVotesERC721.sol | 30 ----------- contracts/interfaces/IVotes.sol | 53 +++++++++++++++++++ contracts/mocks/GovernorMock.sol | 2 +- .../mocks/GovernorTimelockCompoundMock.sol | 2 +- .../mocks/GovernorTimelockControlMock.sol | 2 +- ...norERC721Mock.sol => GovernorVoteMock.sol} | 8 +-- contracts/mocks/wizard/MyGovernor1.sol | 2 +- contracts/mocks/wizard/MyGovernor2.sol | 2 +- contracts/mocks/wizard/MyGovernor3.sol | 2 +- .../token/ERC20/extensions/ERC20Votes.sol | 15 +++--- contracts/utils/Votes.sol | 15 +++--- docs/modules/ROOT/pages/governance.adoc | 2 +- .../extensions/GovernorERC721.test.js | 4 +- 14 files changed, 86 insertions(+), 61 deletions(-) delete mode 100644 contracts/governance/extensions/GovernorVotesERC721.sol create mode 100644 contracts/interfaces/IVotes.sol rename contracts/mocks/{GovernorERC721Mock.sol => GovernorVoteMock.sol} (74%) diff --git a/contracts/governance/extensions/GovernorVotes.sol b/contracts/governance/extensions/GovernorVotes.sol index 86478c0418d..5dc718ac784 100644 --- a/contracts/governance/extensions/GovernorVotes.sol +++ b/contracts/governance/extensions/GovernorVotes.sol @@ -4,17 +4,17 @@ pragma solidity ^0.8.0; import "../Governor.sol"; -import "../../token/ERC20/extensions/ERC20Votes.sol"; +import "../../interfaces/IVotes.sol"; /** - * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token. + * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} or {ERC721Votes} token. * * _Available since v4.3._ */ abstract contract GovernorVotes is Governor { - ERC20Votes public immutable token; + IVotes public immutable token; - constructor(ERC20Votes tokenAddress) { + constructor(IVotes tokenAddress) { token = tokenAddress; } diff --git a/contracts/governance/extensions/GovernorVotesERC721.sol b/contracts/governance/extensions/GovernorVotesERC721.sol deleted file mode 100644 index 37391e708b7..00000000000 --- a/contracts/governance/extensions/GovernorVotesERC721.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.0 (governance/extensions/ERC721GovernorVotesERC721.sol) - -pragma solidity ^0.8.0; - -import "../Governor.sol"; -import "../../token/ERC721/extensions/draft-ERC721Votes.sol"; - -/** - * @dev Extension of {Governor} for voting weight extraction from an {ERC721Votes} token. - * - * _Available since v4.5._ - */ -abstract contract GovernorVotesERC721 is Governor { - ERC721Votes public immutable token; - - /** - * @dev Need the ERC721Votes address to be initialized - */ - constructor(ERC721Votes tokenAddress) { - token = tokenAddress; - } - - /** - * @dev Read the voting weight from the token's built in snapshot mechanism (see {IGovernor-getVotes}). - */ - function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { - return token.getPastVotes(account, blockNumber); - } -} diff --git a/contracts/interfaces/IVotes.sol b/contracts/interfaces/IVotes.sol new file mode 100644 index 00000000000..49428862dce --- /dev/null +++ b/contracts/interfaces/IVotes.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.0 (interfaces/IVotes.sol) +pragma solidity ^0.8.0; + +/** + * @dev Interface of the Votes standard. + * + * _Available since v4.4._ + */ +interface IVotes { + /** + * @dev Returns total amount of votes for account. + */ + function getVotes(address account) external view returns (uint256); + + /** + * @dev Returns total amount of votes at given blockNumber. + */ + function getPastVotes(address account, uint256 blockNumber) external view returns (uint256); + + /** + * @dev Retrieve the `totalVotingPower` at the end of `blockNumber`. Note, this value is the sum of all balances. + * It is but NOT the sum of all the delegated votes! + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastTotalSupply(uint256 blockNumber) external view returns (uint256); + + /** + * @dev Delegate votes from the sender to `delegatee`. + */ + function delegate(address delegatee) external; + + /** + * @dev Returns account delegation. + */ + function delegates(address account) external view returns (address); + + /** + * @dev Delegates votes from signer to `delegatee` + */ + function delegateBySig( + address delegatee, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) external; + +} diff --git a/contracts/mocks/GovernorMock.sol b/contracts/mocks/GovernorMock.sol index cc96dcd2775..85233f55951 100644 --- a/contracts/mocks/GovernorMock.sol +++ b/contracts/mocks/GovernorMock.sol @@ -15,7 +15,7 @@ contract GovernorMock is { constructor( string memory name_, - ERC20Votes token_, + IVotes token_, uint256 votingDelay_, uint256 votingPeriod_, uint256 quorumNumerator_ diff --git a/contracts/mocks/GovernorTimelockCompoundMock.sol b/contracts/mocks/GovernorTimelockCompoundMock.sol index 848f4b409b3..aeba5b86a80 100644 --- a/contracts/mocks/GovernorTimelockCompoundMock.sol +++ b/contracts/mocks/GovernorTimelockCompoundMock.sol @@ -15,7 +15,7 @@ contract GovernorTimelockCompoundMock is { constructor( string memory name_, - ERC20Votes token_, + IVotes token_, uint256 votingDelay_, uint256 votingPeriod_, ICompoundTimelock timelock_, diff --git a/contracts/mocks/GovernorTimelockControlMock.sol b/contracts/mocks/GovernorTimelockControlMock.sol index 4d9e97fd522..97376c8253a 100644 --- a/contracts/mocks/GovernorTimelockControlMock.sol +++ b/contracts/mocks/GovernorTimelockControlMock.sol @@ -15,7 +15,7 @@ contract GovernorTimelockControlMock is { constructor( string memory name_, - ERC20Votes token_, + IVotes token_, uint256 votingDelay_, uint256 votingPeriod_, TimelockController timelock_, diff --git a/contracts/mocks/GovernorERC721Mock.sol b/contracts/mocks/GovernorVoteMock.sol similarity index 74% rename from contracts/mocks/GovernorERC721Mock.sol rename to contracts/mocks/GovernorVoteMock.sol index 7508f168334..617d631df73 100644 --- a/contracts/mocks/GovernorERC721Mock.sol +++ b/contracts/mocks/GovernorVoteMock.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.0; import "../governance/extensions/GovernorCountingSimple.sol"; -import "../governance/extensions/GovernorVotesERC721.sol"; +import "../governance/extensions/GovernorVotes.sol"; -contract GovernorERC721Mock is GovernorVotesERC721, GovernorCountingSimple { - constructor(string memory name_, ERC721Votes token_) Governor(name_) GovernorVotesERC721(token_) {} +contract GovernorVoteMock is GovernorVotes, GovernorCountingSimple { + constructor(string memory name_, IVotes token_) Governor(name_) GovernorVotes(token_) {} function quorum(uint256) public pure override returns (uint256) { return 0; @@ -33,7 +33,7 @@ contract GovernorERC721Mock is GovernorVotesERC721, GovernorCountingSimple { public view virtual - override(IGovernor, GovernorVotesERC721) + override(IGovernor, GovernorVotes) returns (uint256) { return super.getVotes(account, blockNumber); diff --git a/contracts/mocks/wizard/MyGovernor1.sol b/contracts/mocks/wizard/MyGovernor1.sol index 72b486aa782..bd524ee55a2 100644 --- a/contracts/mocks/wizard/MyGovernor1.sol +++ b/contracts/mocks/wizard/MyGovernor1.sol @@ -14,7 +14,7 @@ contract MyGovernor1 is GovernorVotesQuorumFraction, GovernorCountingSimple { - constructor(ERC20Votes _token, TimelockController _timelock) + constructor(IVotes _token, TimelockController _timelock) Governor("MyGovernor") GovernorVotes(_token) GovernorVotesQuorumFraction(4) diff --git a/contracts/mocks/wizard/MyGovernor2.sol b/contracts/mocks/wizard/MyGovernor2.sol index 3f25b91bfe8..3a5c983e0b3 100644 --- a/contracts/mocks/wizard/MyGovernor2.sol +++ b/contracts/mocks/wizard/MyGovernor2.sol @@ -16,7 +16,7 @@ contract MyGovernor2 is GovernorVotesQuorumFraction, GovernorCountingSimple { - constructor(ERC20Votes _token, TimelockController _timelock) + constructor(IVotes _token, TimelockController _timelock) Governor("MyGovernor") GovernorVotes(_token) GovernorVotesQuorumFraction(4) diff --git a/contracts/mocks/wizard/MyGovernor3.sol b/contracts/mocks/wizard/MyGovernor3.sol index c2465751a79..835a893a3a4 100644 --- a/contracts/mocks/wizard/MyGovernor3.sol +++ b/contracts/mocks/wizard/MyGovernor3.sol @@ -14,7 +14,7 @@ contract MyGovernor is GovernorVotes, GovernorVotesQuorumFraction { - constructor(ERC20Votes _token, TimelockController _timelock) + constructor(IVotes _token, TimelockController _timelock) Governor("MyGovernor") GovernorVotes(_token) GovernorVotesQuorumFraction(4) diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index f4fd21d3e5b..ee67d6bf741 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.0; import "./draft-ERC20Permit.sol"; import "../../../utils/math/Math.sol"; +import "../../../interfaces/IVotes.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; @@ -25,7 +26,7 @@ import "../../../utils/cryptography/ECDSA.sol"; * * _Available since v4.2._ */ -abstract contract ERC20Votes is ERC20Permit { +abstract contract ERC20Votes is ERC20Permit, IVotes { struct Checkpoint { uint32 fromBlock; uint224 votes; @@ -65,14 +66,14 @@ abstract contract ERC20Votes is ERC20Permit { /** * @dev Get the address `account` is currently delegating to. */ - function delegates(address account) public view virtual returns (address) { + function delegates(address account) public view virtual override returns (address) { return _delegates[account]; } /** * @dev Gets the current votes balance for `account` */ - function getVotes(address account) public view returns (uint256) { + function getVotes(address account) public view override returns (uint256) { uint256 pos = _checkpoints[account].length; return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes; } @@ -84,7 +85,7 @@ abstract contract ERC20Votes is ERC20Permit { * * - `blockNumber` must have been already mined */ - function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) { + function getPastVotes(address account, uint256 blockNumber) public view override returns (uint256) { require(blockNumber < block.number, "ERC20Votes: block not yet mined"); return _checkpointsLookup(_checkpoints[account], blockNumber); } @@ -97,7 +98,7 @@ abstract contract ERC20Votes is ERC20Permit { * * - `blockNumber` must have been already mined */ - function getPastTotalSupply(uint256 blockNumber) public view returns (uint256) { + function getPastTotalSupply(uint256 blockNumber) public view override returns (uint256) { require(blockNumber < block.number, "ERC20Votes: block not yet mined"); return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber); } @@ -134,7 +135,7 @@ abstract contract ERC20Votes is ERC20Permit { /** * @dev Delegate votes from the sender to `delegatee`. */ - function delegate(address delegatee) public virtual { + function delegate(address delegatee) public virtual override{ _delegate(_msgSender(), delegatee); } @@ -148,7 +149,7 @@ abstract contract ERC20Votes is ERC20Permit { uint8 v, bytes32 r, bytes32 s - ) public virtual { + ) public virtual override{ require(block.timestamp <= expiry, "ERC20Votes: signature expired"); address signer = ECDSA.recover( _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), diff --git a/contracts/utils/Votes.sol b/contracts/utils/Votes.sol index 635193ca37d..b18d103dca0 100644 --- a/contracts/utils/Votes.sol +++ b/contracts/utils/Votes.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import "./Context.sol"; import "./Counters.sol"; import "./Checkpoints.sol"; +import "../interfaces/IVotes.sol"; import "./cryptography/draft-EIP712.sol"; /** @@ -21,7 +22,7 @@ import "./cryptography/draft-EIP712.sol"; * When using this module, the derived contract must implement {_getDelegatorVotingPower}, and can use {_moveVotingPower} * when a delegator's voting power is changed. */ -abstract contract Votes is Context, EIP712 { +abstract contract Votes is Context, EIP712, IVotes { using Checkpoints for Checkpoints.History; using Counters for Counters.Counter; @@ -45,14 +46,14 @@ abstract contract Votes is Context, EIP712 { /** * @dev Returns total amount of votes for account. */ - function getVotes(address account) public view virtual returns (uint256) { + function getVotes(address account) public view virtual override returns (uint256) { return _userCheckpoints[account].latest(); } /** * @dev Returns total amount of votes at given blockNumber. */ - function getPastVotes(address account, uint256 blockNumber) public view virtual returns (uint256) { + function getPastVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { return _userCheckpoints[account].getAtBlock(blockNumber); } @@ -64,7 +65,7 @@ abstract contract Votes is Context, EIP712 { * * - `blockNumber` must have been already mined */ - function getPastTotalSupply(uint256 blockNumber) public view virtual returns (uint256) { + function getPastTotalSupply(uint256 blockNumber) public view virtual override returns (uint256) { require(blockNumber < block.number, "ERC721Votes: block not yet mined"); return _totalCheckpoints.getAtBlock(blockNumber); } @@ -86,7 +87,7 @@ abstract contract Votes is Context, EIP712 { /** * @dev Delegate votes from the sender to `delegatee`. */ - function delegate(address delegatee) public virtual { + function delegate(address delegatee) public virtual override { address delegator = _msgSender(); _delegate(delegator, delegatee); } @@ -94,7 +95,7 @@ abstract contract Votes is Context, EIP712 { /** * @dev Returns account delegation. */ - function delegates(address account) public view virtual returns (address) { + function delegates(address account) public view virtual override returns (address) { return _delegateCheckpoints[account]; } @@ -122,7 +123,7 @@ abstract contract Votes is Context, EIP712 { uint8 v, bytes32 r, bytes32 s - ) public virtual { + ) public virtual override{ require(block.timestamp <= expiry, "ERC721Votes: signature expired"); address signer = ECDSA.recover( _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 98b0c0209ca..5ebcba17b65 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -190,7 +190,7 @@ import "./governance/extensions/GovernorVotesQuorumFraction.sol"; import "./governance/extensions/GovernorTimelockControl.sol"; contract MyGovernor is Governor, GovernorCompatibilityBravo, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl { - constructor(ERC20Votes _token, TimelockController _timelock) + constructor(IVotes _token, TimelockController _timelock) Governor("MyGovernor") GovernorVotes(_token) GovernorVotesQuorumFraction(4) diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 11a0f382b28..eae7b74b924 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -7,7 +7,7 @@ const { } = require('./../GovernorWorkflow.behavior'); const Token = artifacts.require('ERC721VotesMock'); -const Governor = artifacts.require('GovernorERC721Mock'); +const Governor = artifacts.require('GovernorVoteMock'); const CallReceiver = artifacts.require('CallReceiverMock'); contract('GovernorERC721Mock', function (accounts) { @@ -59,7 +59,7 @@ contract('GovernorERC721Mock', function (accounts) { expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); }); - describe('voting with ERC721 token', function () { + describe.only('voting with ERC721 token', function () { beforeEach(async function () { this.settings = { proposal: [ From 052f0647b7559cbebf0d94869d25eb20f6580fb5 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 12:50:17 -0400 Subject: [PATCH 258/300] Remove .only from test --- contracts/interfaces/IVotes.sol | 5 ++--- contracts/token/ERC20/extensions/ERC20Votes.sol | 4 ++-- contracts/utils/Votes.sol | 2 +- test/governance/extensions/GovernorERC721.test.js | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/contracts/interfaces/IVotes.sol b/contracts/interfaces/IVotes.sol index 49428862dce..d06a619e037 100644 --- a/contracts/interfaces/IVotes.sol +++ b/contracts/interfaces/IVotes.sol @@ -10,12 +10,12 @@ pragma solidity ^0.8.0; interface IVotes { /** * @dev Returns total amount of votes for account. - */ + */ function getVotes(address account) external view returns (uint256); /** * @dev Returns total amount of votes at given blockNumber. - */ + */ function getPastVotes(address account, uint256 blockNumber) external view returns (uint256); /** @@ -49,5 +49,4 @@ interface IVotes { bytes32 r, bytes32 s ) external; - } diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index ee67d6bf741..24807a8ceba 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -135,7 +135,7 @@ abstract contract ERC20Votes is ERC20Permit, IVotes { /** * @dev Delegate votes from the sender to `delegatee`. */ - function delegate(address delegatee) public virtual override{ + function delegate(address delegatee) public virtual override { _delegate(_msgSender(), delegatee); } @@ -149,7 +149,7 @@ abstract contract ERC20Votes is ERC20Permit, IVotes { uint8 v, bytes32 r, bytes32 s - ) public virtual override{ + ) public virtual override { require(block.timestamp <= expiry, "ERC20Votes: signature expired"); address signer = ECDSA.recover( _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), diff --git a/contracts/utils/Votes.sol b/contracts/utils/Votes.sol index b18d103dca0..31580af0f5e 100644 --- a/contracts/utils/Votes.sol +++ b/contracts/utils/Votes.sol @@ -123,7 +123,7 @@ abstract contract Votes is Context, EIP712, IVotes { uint8 v, bytes32 r, bytes32 s - ) public virtual override{ + ) public virtual override { require(block.timestamp <= expiry, "ERC721Votes: signature expired"); address signer = ECDSA.recover( _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index eae7b74b924..4c1cc663149 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -59,7 +59,7 @@ contract('GovernorERC721Mock', function (accounts) { expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); }); - describe.only('voting with ERC721 token', function () { + describe('voting with ERC721 token', function () { beforeEach(async function () { this.settings = { proposal: [ From eb6304bc3c97fab34b450e9231b5637392087b32 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 13:12:08 -0400 Subject: [PATCH 259/300] Update contract inheriting from GovernorVotes --- contracts/mocks/GovernorPreventLateQuorumMock.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/mocks/GovernorPreventLateQuorumMock.sol b/contracts/mocks/GovernorPreventLateQuorumMock.sol index 412337c08d5..7de50a01f94 100644 --- a/contracts/mocks/GovernorPreventLateQuorumMock.sol +++ b/contracts/mocks/GovernorPreventLateQuorumMock.sol @@ -17,7 +17,7 @@ contract GovernorPreventLateQuorumMock is constructor( string memory name_, - ERC20Votes token_, + IVotes token_, uint256 votingDelay_, uint256 votingPeriod_, uint256 quorum_, From e5fecaf4d1e0027bf3ce7a7df97b7f4c3f79e27c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 3 Dec 2021 18:36:02 +0100 Subject: [PATCH 260/300] Indentitation/format consistency --- contracts/token/ERC721/ERC721.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index db1bda17bf5..a131d28bf50 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -287,6 +287,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { _owners[tokenId] = to; emit Transfer(address(0), to, tokenId); + _afterTokenTransfer(address(0), to, tokenId); } From 9addc4f4228abd05cdbc4b421f22c71221dc3194 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 13:52:50 -0400 Subject: [PATCH 261/300] Improve format --- contracts/governance/README.adoc | 2 -- contracts/interfaces/IVotes.sol | 8 ++--- contracts/mocks/CheckpointsImpl.sol | 4 --- contracts/mocks/VotingMock.sol | 4 --- contracts/utils/Checkpoints.sol | 7 ---- contracts/utils/Votes.sol | 45 +++++++++++-------------- docs/modules/ROOT/pages/governance.adoc | 4 +-- test/utils/Checkpoints.test.js | 4 --- test/utils/Voting.test.js | 4 +-- 9 files changed, 27 insertions(+), 55 deletions(-) diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index d3248d61bbc..e07f7157685 100644 --- a/contracts/governance/README.adoc +++ b/contracts/governance/README.adoc @@ -68,8 +68,6 @@ NOTE: Functions of the `Governor` contract do not include access control. If you {{GovernorVotesQuorumFraction}} -{{GovernorVotesERC721}} - {{GovernorVotesComp}} === Extensions diff --git a/contracts/interfaces/IVotes.sol b/contracts/interfaces/IVotes.sol index d06a619e037..9bb3daab448 100644 --- a/contracts/interfaces/IVotes.sol +++ b/contracts/interfaces/IVotes.sol @@ -29,14 +29,14 @@ interface IVotes { function getPastTotalSupply(uint256 blockNumber) external view returns (uint256); /** - * @dev Delegate votes from the sender to `delegatee`. + * @dev Returns account delegation. */ - function delegate(address delegatee) external; + function delegates(address account) external view returns (address); /** - * @dev Returns account delegation. + * @dev Delegate votes from the sender to `delegatee`. */ - function delegates(address account) external view returns (address); + function delegate(address delegatee) external; /** * @dev Delegates votes from signer to `delegatee` diff --git a/contracts/mocks/CheckpointsImpl.sol b/contracts/mocks/CheckpointsImpl.sol index 0c772c8e4d1..5b9ec0acbd7 100644 --- a/contracts/mocks/CheckpointsImpl.sol +++ b/contracts/mocks/CheckpointsImpl.sol @@ -9,10 +9,6 @@ contract CheckpointsImpl { Checkpoints.History private _totalCheckpoints; - function length() public view returns (uint256) { - return _totalCheckpoints.length(); - } - function latest() public view returns (uint256) { return _totalCheckpoints.latest(); } diff --git a/contracts/mocks/VotingMock.sol b/contracts/mocks/VotingMock.sol index bdab57be7cc..bb6b6a98ab0 100644 --- a/contracts/mocks/VotingMock.sol +++ b/contracts/mocks/VotingMock.sol @@ -11,10 +11,6 @@ contract VotesMock is Votes { return _getTotalVotes(); } - function getTotalAccountVotes(address account) public view returns (uint256) { - return _getTotalAccountVotes(account); - } - function getTotalVotesAt(uint256 timestamp) public view returns (uint256) { return getPastTotalSupply(timestamp); } diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index cc1bc3d314d..b8a303a5a9b 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -17,13 +17,6 @@ library Checkpoints { Checkpoint[] _checkpoints; } - /** - * @dev Returns checkpoints length. - */ - function length(History storage self) internal view returns (uint256) { - return self._checkpoints.length; - } - /** * @dev Returns total amount of checkpoints. */ diff --git a/contracts/utils/Votes.sol b/contracts/utils/Votes.sol index 31580af0f5e..3e9793deb92 100644 --- a/contracts/utils/Votes.sol +++ b/contracts/utils/Votes.sol @@ -28,11 +28,13 @@ abstract contract Votes is Context, EIP712, IVotes { bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + mapping(address => address) private _delegateCheckpoints; mapping(address => Checkpoints.History) private _userCheckpoints; - mapping(address => Counters.Counter) private _nonces; Checkpoints.History private _totalCheckpoints; + mapping(address => Counters.Counter) private _nonces; + /** * @dev Emitted when an account changes their delegate. */ @@ -78,10 +80,10 @@ abstract contract Votes is Context, EIP712, IVotes { } /** - * @dev Get number of checkpoints for `account` including delegation. + * @dev Returns account delegation. */ - function _getTotalAccountVotes(address account) internal view virtual returns (uint256) { - return _userCheckpoints[account].length(); + function delegates(address account) public view virtual override returns (address) { + return _delegateCheckpoints[account]; } /** @@ -92,27 +94,6 @@ abstract contract Votes is Context, EIP712, IVotes { _delegate(delegator, delegatee); } - /** - * @dev Returns account delegation. - */ - function delegates(address account) public view virtual override returns (address) { - return _delegateCheckpoints[account]; - } - - /** - * @dev Change delegation for `delegator` to `delegatee`. - * - * Emits events {DelegateChanged} and {DelegateVotesChanged}. - */ - function _delegate(address delegator, address newDelegation) internal virtual { - address oldDelegation = delegates(delegator); - _delegateCheckpoints[delegator] = newDelegation; - - emit DelegateChanged(delegator, oldDelegation, newDelegation); - - _moveVotingPower(oldDelegation, newDelegation, _getDelegatorVotingPower(delegator)); - } - /** * @dev Delegates votes from signer to `delegatee` */ @@ -135,6 +116,20 @@ abstract contract Votes is Context, EIP712, IVotes { _delegate(signer, delegatee); } + /** + * @dev Change delegation for `delegator` to `delegatee`. + * + * Emits events {DelegateChanged} and {DelegateVotesChanged}. + */ + function _delegate(address delegator, address newDelegation) internal virtual { + address oldDelegation = delegates(delegator); + _delegateCheckpoints[delegator] = newDelegation; + + emit DelegateChanged(delegator, oldDelegation, newDelegation); + + _moveVotingPower(oldDelegation, newDelegation, _getDelegatorVotingPower(delegator)); + } + /** * @dev Moves voting power. */ diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 5ebcba17b65..0fba214476a 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -161,14 +161,12 @@ NOTE: Voting power could be determined in different ways: multiple ERC20 tokens, Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, 3) what options people have when casting a vote and how those votes are counted, and 4) what type of token should be used to vote. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. -For 1) we will use the GovernorVotes module, which hooks to an ERC20Votes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. +For 1) we will use the GovernorVotes module, which hooks to an IVotes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. For 2) we will use GovernorVotesQuorumFraction which works together with ERC20Votes to define quorum as a percentage of the total supply at the block a proposal’s voting power is retrieved. This requires a constructor parameter to set the percentage. Most Governors nowadays use 4%, so we will initialize the module with parameter 4 (this indicates the percentage, resulting in 4%). For 3) we will use GovernorCountingSimple, a module that offers 3 options to voters: For, Against, and Abstain, and where only For and Abstain votes are counted towards quorum. -For 4) we will use the GovernorVotesERC721 module, which hooks to an ERC721Votes instance to determine the voting power of an account based on the NFTs they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. - Besides these modules, Governor itself has some parameters we must set. votingDelay: How long after a proposal is created should voting power be fixed. A large voting delay gives users time to unstake tokens if necessary. diff --git a/test/utils/Checkpoints.test.js b/test/utils/Checkpoints.test.js index fa087a359dc..1b1d0c43418 100644 --- a/test/utils/Checkpoints.test.js +++ b/test/utils/Checkpoints.test.js @@ -12,10 +12,6 @@ contract('Checkpoints', function (accounts) { this.tx3 = await this.checkpoint.push(3); }); - it('calls length', async function () { - expect(await this.checkpoint.length()).to.be.bignumber.equal('3'); - }); - it('calls latest', async function () { expect(await this.checkpoint.latest()).to.be.bignumber.equal('3'); }); diff --git a/test/utils/Voting.test.js b/test/utils/Voting.test.js index 509ed71c5a0..3278c2ebd7d 100644 --- a/test/utils/Voting.test.js +++ b/test/utils/Voting.test.js @@ -34,8 +34,8 @@ contract('Voting', function (accounts) { expect(await this.voting.delegates(account3)).to.be.equal(account2); }); - it('returns amount of votes for account', async function () { - expect(await this.voting.getTotalAccountVotes(account1)).to.be.bignumber.equal('1'); + it.only('returns amount of votes for account', async function () { + expect(await this.voting.getVotes(account1)).to.be.bignumber.equal('1'); }); }); }); From b8639e761e1f6afc49751ed79b300bdbfca0dcc8 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 13:54:13 -0400 Subject: [PATCH 262/300] Update contracts/utils/Votes.sol Co-authored-by: Francisco Giordano --- contracts/utils/Votes.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/Votes.sol b/contracts/utils/Votes.sol index 3e9793deb92..71e78800acc 100644 --- a/contracts/utils/Votes.sol +++ b/contracts/utils/Votes.sol @@ -22,7 +22,7 @@ import "./cryptography/draft-EIP712.sol"; * When using this module, the derived contract must implement {_getDelegatorVotingPower}, and can use {_moveVotingPower} * when a delegator's voting power is changed. */ -abstract contract Votes is Context, EIP712, IVotes { +abstract contract Votes is Context, IVotes, EIP712 { using Checkpoints for Checkpoints.History; using Counters for Counters.Counter; From f5f6aab0d9069bc7d3ddbd1eb9ca7803ae7d14ad Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 13:54:38 -0400 Subject: [PATCH 263/300] Update contracts/token/ERC20/extensions/ERC20Votes.sol Co-authored-by: Francisco Giordano --- contracts/token/ERC20/extensions/ERC20Votes.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index 24807a8ceba..90c8d191c3f 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -26,7 +26,7 @@ import "../../../utils/cryptography/ECDSA.sol"; * * _Available since v4.2._ */ -abstract contract ERC20Votes is ERC20Permit, IVotes { +abstract contract ERC20Votes is IVotes, ERC20Permit { struct Checkpoint { uint32 fromBlock; uint224 votes; From 87bd7b203cc0d9a4a4e36d34016a14b4f085a89e Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 14:06:06 -0400 Subject: [PATCH 264/300] Update contracts/utils/Checkpoints.sol Co-authored-by: Francisco Giordano --- contracts/utils/Checkpoints.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index b8a303a5a9b..782d93d2e6a 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -18,7 +18,7 @@ library Checkpoints { } /** - * @dev Returns total amount of checkpoints. + * @dev Returns the value in the latest checkpoint. */ function latest(History storage self) internal view returns (uint256) { uint256 pos = self._checkpoints.length; From 93a2079cc44744e61975d41c7efec5509d476631 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 14:32:26 -0400 Subject: [PATCH 265/300] Update IVotes and Votes directory --- contracts/governance/extensions/GovernorVotes.sol | 2 +- contracts/{interfaces => governance/utils}/IVotes.sol | 0 contracts/{ => governance}/utils/Votes.sol | 10 +++++----- contracts/mocks/GovernorVoteMock.sol | 2 +- contracts/mocks/VotingMock.sol | 6 +----- contracts/token/ERC20/extensions/ERC20Votes.sol | 2 +- .../token/ERC721/extensions/draft-ERC721Votes.sol | 6 +----- test/governance/GovernorWorkflow.behavior.js | 2 +- test/governance/extensions/GovernorERC721.test.js | 2 +- test/utils/Voting.test.js | 6 +++--- 10 files changed, 15 insertions(+), 23 deletions(-) rename contracts/{interfaces => governance/utils}/IVotes.sol (100%) rename contracts/{ => governance}/utils/Votes.sol (97%) diff --git a/contracts/governance/extensions/GovernorVotes.sol b/contracts/governance/extensions/GovernorVotes.sol index 5dc718ac784..b60b12e6d18 100644 --- a/contracts/governance/extensions/GovernorVotes.sol +++ b/contracts/governance/extensions/GovernorVotes.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import "../Governor.sol"; -import "../../interfaces/IVotes.sol"; +import "../utils/IVotes.sol"; /** * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} or {ERC721Votes} token. diff --git a/contracts/interfaces/IVotes.sol b/contracts/governance/utils/IVotes.sol similarity index 100% rename from contracts/interfaces/IVotes.sol rename to contracts/governance/utils/IVotes.sol diff --git a/contracts/utils/Votes.sol b/contracts/governance/utils/Votes.sol similarity index 97% rename from contracts/utils/Votes.sol rename to contracts/governance/utils/Votes.sol index 71e78800acc..c37ce39f159 100644 --- a/contracts/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import "./Context.sol"; -import "./Counters.sol"; -import "./Checkpoints.sol"; -import "../interfaces/IVotes.sol"; -import "./cryptography/draft-EIP712.sol"; +import "../../utils/Context.sol"; +import "../../utils/Counters.sol"; +import "../../utils/Checkpoints.sol"; +import "./IVotes.sol"; +import "../../utils/cryptography/draft-EIP712.sol"; /** * @dev Voting operations. diff --git a/contracts/mocks/GovernorVoteMock.sol b/contracts/mocks/GovernorVoteMock.sol index 617d631df73..23ccf6bc09c 100644 --- a/contracts/mocks/GovernorVoteMock.sol +++ b/contracts/mocks/GovernorVoteMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "../governance/extensions/GovernorCountingSimple.sol"; import "../governance/extensions/GovernorVotes.sol"; -contract GovernorVoteMock is GovernorVotes, GovernorCountingSimple { +contract GovernorVoteMocks is GovernorVotes, GovernorCountingSimple { constructor(string memory name_, IVotes token_) Governor(name_) GovernorVotes(token_) {} function quorum(uint256) public pure override returns (uint256) { diff --git a/contracts/mocks/VotingMock.sol b/contracts/mocks/VotingMock.sol index bb6b6a98ab0..d689a2d1afa 100644 --- a/contracts/mocks/VotingMock.sol +++ b/contracts/mocks/VotingMock.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; -import "../utils/Votes.sol"; +import "../governance/utils/Votes.sol"; contract VotesMock is Votes { constructor(string memory name) EIP712(name, "1") {} @@ -11,10 +11,6 @@ contract VotesMock is Votes { return _getTotalVotes(); } - function getTotalVotesAt(uint256 timestamp) public view returns (uint256) { - return getPastTotalSupply(timestamp); - } - function delegate(address account, address newDelegation) public { return _delegate(account, newDelegation); } diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index 90c8d191c3f..a0f8fc64c24 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "./draft-ERC20Permit.sol"; import "../../../utils/math/Math.sol"; -import "../../../interfaces/IVotes.sol"; +import "../../../governance/utils/IVotes.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 3f31e44bc03..f49d622ba93 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -4,11 +4,7 @@ pragma solidity ^0.8.0; import "../ERC721.sol"; -import "../../../utils/Votes.sol"; -import "../../../utils/math/Math.sol"; -import "../../../utils/Checkpoints.sol"; -import "../../../utils/math/SafeCast.sol"; -import "../../../utils/cryptography/ECDSA.sol"; +import "../../../governance/utils/Votes.sol"; /** * @dev Extension of ERC721 to support Compound-like voting and delegation. This version is more generic than Compound's, diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js index 7bcaed06abf..e35adbc8461 100644 --- a/test/governance/GovernorWorkflow.behavior.js +++ b/test/governance/GovernorWorkflow.behavior.js @@ -25,7 +25,7 @@ function runGovernorWorkflow () { this.id = await this.mock.hashProposal(...this.settings.proposal.slice(0, -1), this.descriptionHash); }); - it('run', async function () { + it.only('run', async function () { // transfer tokens if (tryGet(this.settings, 'voters')) { for (const voter of this.settings.voters) { diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 4c1cc663149..3f89c02b4e0 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -7,7 +7,7 @@ const { } = require('./../GovernorWorkflow.behavior'); const Token = artifacts.require('ERC721VotesMock'); -const Governor = artifacts.require('GovernorVoteMock'); +const Governor = artifacts.require('GovernorVoteMocks'); const CallReceiver = artifacts.require('CallReceiverMock'); contract('GovernorERC721Mock', function (accounts) { diff --git a/test/utils/Voting.test.js b/test/utils/Voting.test.js index 3278c2ebd7d..46e39898abf 100644 --- a/test/utils/Voting.test.js +++ b/test/utils/Voting.test.js @@ -14,7 +14,7 @@ contract('Voting', function (accounts) { expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('0'); }); - describe('move voting power', function () { + describe.only('move voting power', function () { beforeEach(async function () { this.tx1 = await this.voting.giveVotingPower(account1, 1); this.tx2 = await this.voting.giveVotingPower(account2, 1); @@ -23,7 +23,7 @@ contract('Voting', function (accounts) { it('reverts if block number >= current block', async function () { await expectRevert( - this.voting.getTotalVotesAt(this.tx3.receipt.blockNumber + 1), + this.voting.getPastTotalSupply(this.tx3.receipt.blockNumber + 1), 'block not yet mined', ); }); @@ -34,7 +34,7 @@ contract('Voting', function (accounts) { expect(await this.voting.delegates(account3)).to.be.equal(account2); }); - it.only('returns amount of votes for account', async function () { + it('returns amount of votes for account', async function () { expect(await this.voting.getVotes(account1)).to.be.bignumber.equal('1'); }); }); From 829f074f64747a3d035f41ef0ffd1e0fb254c1ec Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 14:52:11 -0400 Subject: [PATCH 266/300] Remove .only from test files --- test/governance/GovernorWorkflow.behavior.js | 2 +- test/utils/Voting.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js index e35adbc8461..7bcaed06abf 100644 --- a/test/governance/GovernorWorkflow.behavior.js +++ b/test/governance/GovernorWorkflow.behavior.js @@ -25,7 +25,7 @@ function runGovernorWorkflow () { this.id = await this.mock.hashProposal(...this.settings.proposal.slice(0, -1), this.descriptionHash); }); - it.only('run', async function () { + it('run', async function () { // transfer tokens if (tryGet(this.settings, 'voters')) { for (const voter of this.settings.voters) { diff --git a/test/utils/Voting.test.js b/test/utils/Voting.test.js index 46e39898abf..4110c0f85b4 100644 --- a/test/utils/Voting.test.js +++ b/test/utils/Voting.test.js @@ -14,7 +14,7 @@ contract('Voting', function (accounts) { expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('0'); }); - describe.only('move voting power', function () { + describe('move voting power', function () { beforeEach(async function () { this.tx1 = await this.voting.giveVotingPower(account1, 1); this.tx2 = await this.voting.giveVotingPower(account2, 1); From 8fdb1e0ed0b14156a4f3cd5683fac004cadd6cf2 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 14:55:48 -0400 Subject: [PATCH 267/300] Update storage variables names --- contracts/governance/utils/Votes.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index c37ce39f159..b198d7c1f7c 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -29,8 +29,8 @@ abstract contract Votes is Context, IVotes, EIP712 { bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); - mapping(address => address) private _delegateCheckpoints; - mapping(address => Checkpoints.History) private _userCheckpoints; + mapping(address => address) private _delegation; + mapping(address => Checkpoints.History) private _delegateCheckpoints; Checkpoints.History private _totalCheckpoints; mapping(address => Counters.Counter) private _nonces; @@ -49,14 +49,14 @@ abstract contract Votes is Context, IVotes, EIP712 { * @dev Returns total amount of votes for account. */ function getVotes(address account) public view virtual override returns (uint256) { - return _userCheckpoints[account].latest(); + return _delegateCheckpoints[account].latest(); } /** * @dev Returns total amount of votes at given blockNumber. */ function getPastVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { - return _userCheckpoints[account].getAtBlock(blockNumber); + return _delegateCheckpoints[account].getAtBlock(blockNumber); } /** @@ -83,7 +83,7 @@ abstract contract Votes is Context, IVotes, EIP712 { * @dev Returns account delegation. */ function delegates(address account) public view virtual override returns (address) { - return _delegateCheckpoints[account]; + return _delegation[account]; } /** @@ -123,7 +123,7 @@ abstract contract Votes is Context, IVotes, EIP712 { */ function _delegate(address delegator, address newDelegation) internal virtual { address oldDelegation = delegates(delegator); - _delegateCheckpoints[delegator] = newDelegation; + _delegation[delegator] = newDelegation; emit DelegateChanged(delegator, oldDelegation, newDelegation); @@ -142,13 +142,13 @@ abstract contract Votes is Context, IVotes, EIP712 { if (from == address(0)) { _totalCheckpoints.push(_add, amount); } else { - (uint256 oldValue, uint256 newValue) = _userCheckpoints[from].push(_subtract, amount); + (uint256 oldValue, uint256 newValue) = _delegateCheckpoints[from].push(_subtract, amount); emit DelegateVotesChanged(from, oldValue, newValue); } if (to == address(0)) { _totalCheckpoints.push(_subtract, amount); } else { - (uint256 oldValue, uint256 newValue) = _userCheckpoints[to].push(_add, amount); + (uint256 oldValue, uint256 newValue) = _delegateCheckpoints[to].push(_add, amount); emit DelegateVotesChanged(to, oldValue, newValue); } } From dfe225c398814b1e3d7991043f3a473d570527c6 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 3 Dec 2021 15:08:59 -0400 Subject: [PATCH 268/300] Change inheritance order --- contracts/governance/utils/Votes.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index b198d7c1f7c..4b4f8071b69 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -22,7 +22,7 @@ import "../../utils/cryptography/draft-EIP712.sol"; * When using this module, the derived contract must implement {_getDelegatorVotingPower}, and can use {_moveVotingPower} * when a delegator's voting power is changed. */ -abstract contract Votes is Context, IVotes, EIP712 { +abstract contract Votes is IVotes, Context, EIP712 { using Checkpoints for Checkpoints.History; using Counters for Counters.Counter; From 1cca7be37019373256a6d2911a6b534e731938ff Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 6 Dec 2021 07:58:41 -0400 Subject: [PATCH 269/300] Add function to handle mint and burn before moving voting power --- contracts/governance/utils/Votes.sol | 27 ++++++++++++++----- contracts/mocks/VotingMock.sol | 2 +- .../ERC721/extensions/draft-ERC721Votes.sol | 2 +- .../ERC721/extensions/ERC721Votes.test.js | 2 +- test/utils/Voting.test.js | 4 +-- 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index 4b4f8071b69..f52bd2e4b0f 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -130,6 +130,23 @@ abstract contract Votes is IVotes, Context, EIP712 { _moveVotingPower(oldDelegation, newDelegation, _getDelegatorVotingPower(delegator)); } + /** + * @dev Transfers voting assets. + */ + function _transferVotingAssets( + address from, + address to, + uint256 amount + ) internal virtual { + if (from == address(0)) { + _totalCheckpoints.push(_add, amount); + } + if (to == address(0)) { + _totalCheckpoints.push(_subtract, amount); + } + _moveVotingPower(delegates(from), delegates(to), amount); + } + /** * @dev Moves voting power. */ @@ -137,17 +154,13 @@ abstract contract Votes is IVotes, Context, EIP712 { address from, address to, uint256 amount - ) internal virtual { + ) private { if (from != to && amount > 0) { - if (from == address(0)) { - _totalCheckpoints.push(_add, amount); - } else { + if (from != address(0)) { (uint256 oldValue, uint256 newValue) = _delegateCheckpoints[from].push(_subtract, amount); emit DelegateVotesChanged(from, oldValue, newValue); } - if (to == address(0)) { - _totalCheckpoints.push(_subtract, amount); - } else { + if (to != address(0)) { (uint256 oldValue, uint256 newValue) = _delegateCheckpoints[to].push(_add, amount); emit DelegateVotesChanged(to, oldValue, newValue); } diff --git a/contracts/mocks/VotingMock.sol b/contracts/mocks/VotingMock.sol index d689a2d1afa..8ffac03ab31 100644 --- a/contracts/mocks/VotingMock.sol +++ b/contracts/mocks/VotingMock.sol @@ -20,6 +20,6 @@ contract VotesMock is Votes { } function giveVotingPower(address account, uint8 amount) external { - _moveVotingPower(address(0), account, amount); + _transferVotingAssets(address(0), account, amount); } } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index f49d622ba93..b2423e35507 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -33,7 +33,7 @@ abstract contract ERC721Votes is ERC721, Votes { uint256 tokenId ) internal virtual override { super._afterTokenTransfer(from, to, tokenId); - _moveVotingPower(delegates(from), delegates(to), 1); + _transferVotingAssets(from, to, 1); } /** diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 917c08f683c..f7ed14018e2 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -453,7 +453,7 @@ contract('ERC721Votes', function (accounts) { it('returns the same total supply on transfers', async function () { await this.token.delegate(holder, { from: holder }); - await this.token.delegate(recipient, { from: recipient }); + //await this.token.delegate(recipient, { from: recipient }); const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); diff --git a/test/utils/Voting.test.js b/test/utils/Voting.test.js index 4110c0f85b4..b777cb11f13 100644 --- a/test/utils/Voting.test.js +++ b/test/utils/Voting.test.js @@ -34,8 +34,8 @@ contract('Voting', function (accounts) { expect(await this.voting.delegates(account3)).to.be.equal(account2); }); - it('returns amount of votes for account', async function () { - expect(await this.voting.getVotes(account1)).to.be.bignumber.equal('1'); + it('returns total amount of votes', async function () { + expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('3'); }); }); }); From c1737497dd9d61a77ecc128837ab651e9d74ff28 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Mon, 6 Dec 2021 09:47:06 -0300 Subject: [PATCH 270/300] Update README.adoc --- contracts/governance/README.adoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index e07f7157685..d94bde71e96 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} token. -* {GovernorVotesERC721}: Extracts voting weight from 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. From f2cccc31ecf8237327d4b1810e39a389f8ea3517 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Mon, 6 Dec 2021 09:52:23 -0300 Subject: [PATCH 271/300] rename files to new names --- contracts/mocks/{VotingMock.sol => VotesMock.sol} | 0 test/{utils/Voting.test.js => governance/utils/Votes.test.js} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename contracts/mocks/{VotingMock.sol => VotesMock.sol} (100%) rename test/{utils/Voting.test.js => governance/utils/Votes.test.js} (100%) diff --git a/contracts/mocks/VotingMock.sol b/contracts/mocks/VotesMock.sol similarity index 100% rename from contracts/mocks/VotingMock.sol rename to contracts/mocks/VotesMock.sol diff --git a/test/utils/Voting.test.js b/test/governance/utils/Votes.test.js similarity index 100% rename from test/utils/Voting.test.js rename to test/governance/utils/Votes.test.js From 2f8cf7bd78fdc4080adb769e146322fa284b0072 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 6 Dec 2021 09:01:14 -0400 Subject: [PATCH 272/300] Rename function getTotalSupply --- contracts/governance/utils/Votes.sol | 2 +- contracts/mocks/VotingMock.sol | 4 ++-- contracts/token/ERC721/extensions/draft-ERC721Votes.sol | 2 +- test/utils/Voting.test.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index f52bd2e4b0f..578cb97c5f4 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -75,7 +75,7 @@ abstract contract Votes is IVotes, Context, EIP712 { /** * @dev Returns total amount of votes. */ - function _getTotalVotes() internal view virtual returns (uint256) { + function _getTotalSupply() internal view virtual returns (uint256) { return _totalCheckpoints.latest(); } diff --git a/contracts/mocks/VotingMock.sol b/contracts/mocks/VotingMock.sol index 8ffac03ab31..d7d73b929f8 100644 --- a/contracts/mocks/VotingMock.sol +++ b/contracts/mocks/VotingMock.sol @@ -7,8 +7,8 @@ import "../governance/utils/Votes.sol"; contract VotesMock is Votes { constructor(string memory name) EIP712(name, "1") {} - function getTotalVotes() public view returns (uint256) { - return _getTotalVotes(); + function getTotalSupply() public view returns (uint256) { + return _getTotalSupply(); } function delegate(address account, address newDelegation) public { diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index b2423e35507..25ea82271de 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -32,8 +32,8 @@ abstract contract ERC721Votes is ERC721, Votes { address to, uint256 tokenId ) internal virtual override { - super._afterTokenTransfer(from, to, tokenId); _transferVotingAssets(from, to, 1); + super._afterTokenTransfer(from, to, tokenId); } /** diff --git a/test/utils/Voting.test.js b/test/utils/Voting.test.js index b777cb11f13..3eee40df82d 100644 --- a/test/utils/Voting.test.js +++ b/test/utils/Voting.test.js @@ -35,7 +35,7 @@ contract('Voting', function (accounts) { }); it('returns total amount of votes', async function () { - expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('3'); + expect(await this.voting.getTotalSupply()).to.be.bignumber.equal('3'); }); }); }); From edf795b7495455068b36e0b40964ee1f669ece52 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 6 Dec 2021 09:29:29 -0400 Subject: [PATCH 273/300] Change test function name --- test/governance/utils/Votes.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/governance/utils/Votes.test.js b/test/governance/utils/Votes.test.js index 3eee40df82d..464dbe379a7 100644 --- a/test/governance/utils/Votes.test.js +++ b/test/governance/utils/Votes.test.js @@ -11,7 +11,7 @@ contract('Voting', function (accounts) { }); it('starts with zero votes', async function () { - expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('0'); + expect(await this.voting.getTotalSupply()).to.be.bignumber.equal('0'); }); describe('move voting power', function () { From 7f90b88d57665498129940614469f0bebaf8b160 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 6 Dec 2021 12:22:42 -0400 Subject: [PATCH 274/300] Fix documentation after function rename --- contracts/governance/utils/Votes.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index 578cb97c5f4..8e487b8f8e8 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -60,7 +60,7 @@ abstract contract Votes is IVotes, Context, EIP712 { } /** - * @dev Retrieve the `totalVotingPower` at the end of `blockNumber`. Note, this value is the sum of all balances. + * @dev Retrieve the votes total supply at the end of `blockNumber`. Note, this value is the sum of all balances. * It is but NOT the sum of all the delegated votes! * * Requirements: From 7cda9a3d90566c95d49a37b82871ab2436dfe521 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Mon, 6 Dec 2021 15:08:55 -0300 Subject: [PATCH 275/300] improve documentation --- .../governance/extensions/GovernorVotes.sol | 2 +- .../ERC721/extensions/draft-ERC721Votes.sol | 11 +++--- docs/modules/ROOT/pages/governance.adoc | 38 +------------------ 3 files changed, 7 insertions(+), 44 deletions(-) diff --git a/contracts/governance/extensions/GovernorVotes.sol b/contracts/governance/extensions/GovernorVotes.sol index b60b12e6d18..bdca8c935bd 100644 --- a/contracts/governance/extensions/GovernorVotes.sol +++ b/contracts/governance/extensions/GovernorVotes.sol @@ -7,7 +7,7 @@ import "../Governor.sol"; import "../utils/IVotes.sol"; /** - * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} or {ERC721Votes} token. + * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token, or since v4.5 an {ERC721Votes} token. * * _Available since v4.3._ */ diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 25ea82271de..9069ba2a0b4 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -7,16 +7,15 @@ import "../ERC721.sol"; import "../../../governance/utils/Votes.sol"; /** - * @dev Extension of ERC721 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. + * @dev Extension of ERC721 to support voting and delegation. Each individual NFT counts for 1 vote. * * 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 {getVotes} and {getPastVotes}. + * 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 `getVotes` and `getPastVotes`. * * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. - * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this + * Enabling self-delegation can easily be done by overriding the `delegates` function. Keep in mind however that this * will significantly increase the base gas cost of transfers. * * _Available since v4.5._ @@ -25,7 +24,7 @@ abstract contract ERC721Votes is ERC721, Votes { /** * @dev Move voting power when tokens are transferred. * - * Emits a {DelegateVotesChanged} event. + * Emits a {Votes-DelegateVotesChanged} event. */ function _afterTokenTransfer( address from, diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 0fba214476a..9f04ab5974e 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -18,10 +18,6 @@ OpenZeppelin’s Governor system was designed with a concern for compatibility w The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. -=== ERC721Votes - -The ERC721 extension to keep track of votes and vote delegation is one such case. - === Governor & GovernorCompatibilityBravo An OpenZeppelin Governor contract is by default not interface-compatible with GovernorAlpha or Bravo, since some of the functions are different or missing, although it shares all of the same events. However, it’s possible to opt in to full compatibility by inheriting from the GovernorCompatibilityBravo module. The contract will be cheaper to deploy and use without this module. @@ -123,39 +119,7 @@ contract MyToken is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper { } ``` -If your project requires The voting power of each account in our governance setup will be determined by an ERC721 token. The token has to implement the ERC721Votes extension. This extension will keep track of historical balances so that voting power is retrieved from past snapshots rather than current balance, which is an important protection that prevents double voting. - -```solidity -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "openzeppelin/contracts/token/ERC721/extensions/draft-ERC721Votes.sol"; - -contract MyToken is ERC721Votes { - constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer( - address from, - address to, - uint256 tokenId - ) internal override(ERC721Votes) { - super._afterTokenTransfer(from, to, tokenId); - } - - function _mint(address to, uint256 tokenId) internal override(ERC721Votes) { - super._mint(to, tokenId); - } - - function _burn(uint256 tokenId) internal override(ERC721Votes) { - super._burn(tokenId); - } -} -``` - -NOTE: Voting power could be determined in different ways: multiple ERC20 tokens, ERC721 tokens, sybil resistant identities, etc. All of these options are potentially supported by writing a custom Votes module for your Governor. +NOTE: Voting power could be determined in different ways: multiple ERC20 tokens, ERC721 tokens, sybil resistant identities, etc. All of these options are potentially supported by writing a custom Votes module for your Governor. The only other source of voting power available in OpenZeppelin Contracts currently is xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`]. === Governor From a82fc9cd919ea80f05c017682aa84366e5b1ce2e Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 6 Dec 2021 15:19:28 -0400 Subject: [PATCH 276/300] Update Votes.sol documentation --- contracts/governance/README.adoc | 5 +++++ contracts/governance/utils/Votes.sol | 10 ++++++---- contracts/utils/Checkpoints.sol | 6 +++++- contracts/utils/README.adoc | 2 ++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index d94bde71e96..0c23257e3a1 100644 --- a/contracts/governance/README.adoc +++ b/contracts/governance/README.adoc @@ -84,12 +84,17 @@ NOTE: Functions of the `Governor` contract do not include access control. If you {{GovernorProposalThreshold}} +== Utils + +{{Votes}} + == Timelock In a governance system, the {TimelockController} contract is in charge of introducing a delay between a proposal and its execution. It can be used with or without a {Governor}. {{TimelockController}} + [[timelock-terminology]] ==== Terminology diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index 8e487b8f8e8..b85312ce113 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -8,7 +8,8 @@ import "./IVotes.sol"; import "../../utils/cryptography/draft-EIP712.sol"; /** - * @dev Voting operations. + * @dev This is a base abstract contract that tracks voting power for a set of accounts with a vote delegation system. + * It can be combined with a token contract to represent voting power as the token unit, see {ERC721Votes}. * * 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 @@ -19,8 +20,10 @@ import "../../utils/cryptography/draft-EIP712.sol"; * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this * will significantly increase the base gas cost of transfers. * - * When using this module, the derived contract must implement {_getDelegatorVotingPower}, and can use {_moveVotingPower} + * When using this module, the derived contract must implement {_getDelegatorVotingPower}, and can use {_transferVotingAssets} * when a delegator's voting power is changed. + * + * _Available since v4.5._ */ abstract contract Votes is IVotes, Context, EIP712 { using Checkpoints for Checkpoints.History; @@ -53,7 +56,7 @@ abstract contract Votes is IVotes, Context, EIP712 { } /** - * @dev Returns total amount of votes at given blockNumber. + * @dev Returns total amount of votes at given blockNumber for account. */ function getPastVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { return _delegateCheckpoints[account].getAtBlock(blockNumber); @@ -184,7 +187,6 @@ abstract contract Votes is IVotes, Context, EIP712 { /** * @dev "Consume a nonce": return the current value and increment. * - * _Available since v4.1._ */ function _useNonce(address owner) internal virtual returns (uint256 current) { Counters.Counter storage nonce = _nonces[owner]; diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index 782d93d2e6a..c0af0a41440 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -5,7 +5,11 @@ import "./math/Math.sol"; import "./math/SafeCast.sol"; /** - * @dev Checkpoints operations. + * @dev This library defines the `History` struct, for checkpointing values as they change at different points in + * time, and later looking up past values by block number. See {Votes}. + * + * To create a history of checkpoints define a variable type `Checkpoints.History` in your contract. Using the + * {push} function you can store checkpoints corresponding to the current transaction block. */ library Checkpoints { struct Checkpoint { diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 4edcf923bb1..78f44b61858 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -86,6 +86,8 @@ Note that, in all cases, accounts simply _declare_ their interfaces, but they ar {{EnumerableSet}} +{{Checkpoints}} + == Libraries {{Create2}} From b171720c680f8450d97ec90180fe1a49618fd644 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 6 Dec 2021 15:42:09 -0400 Subject: [PATCH 277/300] Add Chekpoints.sol documentation --- contracts/governance/utils/Votes.sol | 2 +- contracts/utils/Checkpoints.sol | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index b85312ce113..e84ab4494b4 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.0; import "../../utils/Context.sol"; import "../../utils/Counters.sol"; import "../../utils/Checkpoints.sol"; -import "./IVotes.sol"; import "../../utils/cryptography/draft-EIP712.sol"; +import "./IVotes.sol"; /** * @dev This is a base abstract contract that tracks voting power for a set of accounts with a vote delegation system. diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index c0af0a41440..6dd401cfe08 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -8,8 +8,10 @@ import "./math/SafeCast.sol"; * @dev This library defines the `History` struct, for checkpointing values as they change at different points in * time, and later looking up past values by block number. See {Votes}. * - * To create a history of checkpoints define a variable type `Checkpoints.History` in your contract. Using the - * {push} function you can store checkpoints corresponding to the current transaction block. + * To create a history of checkpoints define a variable type `Checkpoints.History` in your contract, and store new + * checkpoints or the current transaction block using the {push} function. + * + * _Available since v4.5._ */ library Checkpoints { struct Checkpoint { @@ -49,7 +51,10 @@ library Checkpoints { } /** - * @dev Creates checkpoint + * @dev Creates checkpoint if History is `empty`, or if the current `blockNumber` is higher than the latest + * position `_blockNumber`, otherwise will change the stored value for current block. + * + * Returns previous value and current value. */ function push(History storage self, uint256 value) internal returns (uint256, uint256) { uint256 pos = self._checkpoints.length; @@ -65,7 +70,7 @@ library Checkpoints { } /** - * @dev Creates checkpoint + * @dev Creates checkpoint using the given `op` to compute `value`. */ function push( History storage self, From 7e134d856557eef8ac23c930ba91a11d261f7cba Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 6 Dec 2021 15:50:52 -0400 Subject: [PATCH 278/300] Improve ERc721 documentation --- contracts/governance/utils/Votes.sol | 3 ++- contracts/token/ERC721/extensions/draft-ERC721Votes.sol | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index e84ab4494b4..f52d5b279ca 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -185,8 +185,9 @@ abstract contract Votes is IVotes, Context, EIP712 { } /** - * @dev "Consume a nonce": return the current value and increment. + * @dev Consumes a nonce. * + * Returns the current value and increments nonce. */ function _useNonce(address owner) internal virtual returns (uint256 current) { Counters.Counter storage nonce = _nonces[owner]; diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index 9069ba2a0b4..dafd4eb7be9 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -7,7 +7,7 @@ import "../ERC721.sol"; import "../../../governance/utils/Votes.sol"; /** - * @dev Extension of ERC721 to support voting and delegation. Each individual NFT counts for 1 vote. + * @dev Extension of ERC721 to support voting and delegation, where each individual NFT counts as 1 vote. * * 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 From 1197d84f2e348158bdafbd4c741f4a24bcd2c554 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Mon, 6 Dec 2021 16:21:00 -0400 Subject: [PATCH 279/300] Erc721 governance documentation improvement (#21) * Add function to handle mint and burn before moving voting power * Update README.adoc * rename files to new names * Rename function getTotalSupply * Change test function name * Fix documentation after function rename * improve documentation * Update Votes.sol documentation * Add Chekpoints.sol documentation * Improve ERc721 documentation Co-authored-by: Francisco Giordano --- contracts/governance/README.adoc | 7 ++- .../governance/extensions/GovernorVotes.sol | 2 +- contracts/governance/utils/Votes.sol | 46 +++++++++++++------ .../mocks/{VotingMock.sol => VotesMock.sol} | 6 +-- .../ERC721/extensions/draft-ERC721Votes.sol | 13 +++--- contracts/utils/Checkpoints.sol | 15 ++++-- contracts/utils/README.adoc | 2 + docs/modules/ROOT/pages/governance.adoc | 38 +-------------- .../utils/Votes.test.js} | 6 +-- .../ERC721/extensions/ERC721Votes.test.js | 2 +- 10 files changed, 65 insertions(+), 72 deletions(-) rename contracts/mocks/{VotingMock.sol => VotesMock.sol} (76%) rename test/{utils/Voting.test.js => governance/utils/Votes.test.js} (83%) diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index e07f7157685..0c23257e3a1 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} token. -* {GovernorVotesERC721}: Extracts voting weight from 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. @@ -86,12 +84,17 @@ NOTE: Functions of the `Governor` contract do not include access control. If you {{GovernorProposalThreshold}} +== Utils + +{{Votes}} + == Timelock In a governance system, the {TimelockController} contract is in charge of introducing a delay between a proposal and its execution. It can be used with or without a {Governor}. {{TimelockController}} + [[timelock-terminology]] ==== Terminology diff --git a/contracts/governance/extensions/GovernorVotes.sol b/contracts/governance/extensions/GovernorVotes.sol index b60b12e6d18..bdca8c935bd 100644 --- a/contracts/governance/extensions/GovernorVotes.sol +++ b/contracts/governance/extensions/GovernorVotes.sol @@ -7,7 +7,7 @@ import "../Governor.sol"; import "../utils/IVotes.sol"; /** - * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} or {ERC721Votes} token. + * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token, or since v4.5 an {ERC721Votes} token. * * _Available since v4.3._ */ diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index 4b4f8071b69..f52d5b279ca 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -4,11 +4,12 @@ pragma solidity ^0.8.0; import "../../utils/Context.sol"; import "../../utils/Counters.sol"; import "../../utils/Checkpoints.sol"; -import "./IVotes.sol"; import "../../utils/cryptography/draft-EIP712.sol"; +import "./IVotes.sol"; /** - * @dev Voting operations. + * @dev This is a base abstract contract that tracks voting power for a set of accounts with a vote delegation system. + * It can be combined with a token contract to represent voting power as the token unit, see {ERC721Votes}. * * 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 @@ -19,8 +20,10 @@ import "../../utils/cryptography/draft-EIP712.sol"; * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this * will significantly increase the base gas cost of transfers. * - * When using this module, the derived contract must implement {_getDelegatorVotingPower}, and can use {_moveVotingPower} + * When using this module, the derived contract must implement {_getDelegatorVotingPower}, and can use {_transferVotingAssets} * when a delegator's voting power is changed. + * + * _Available since v4.5._ */ abstract contract Votes is IVotes, Context, EIP712 { using Checkpoints for Checkpoints.History; @@ -53,14 +56,14 @@ abstract contract Votes is IVotes, Context, EIP712 { } /** - * @dev Returns total amount of votes at given blockNumber. + * @dev Returns total amount of votes at given blockNumber for account. */ function getPastVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { return _delegateCheckpoints[account].getAtBlock(blockNumber); } /** - * @dev Retrieve the `totalVotingPower` at the end of `blockNumber`. Note, this value is the sum of all balances. + * @dev Retrieve the votes total supply at the end of `blockNumber`. Note, this value is the sum of all balances. * It is but NOT the sum of all the delegated votes! * * Requirements: @@ -75,7 +78,7 @@ abstract contract Votes is IVotes, Context, EIP712 { /** * @dev Returns total amount of votes. */ - function _getTotalVotes() internal view virtual returns (uint256) { + function _getTotalSupply() internal view virtual returns (uint256) { return _totalCheckpoints.latest(); } @@ -130,6 +133,23 @@ abstract contract Votes is IVotes, Context, EIP712 { _moveVotingPower(oldDelegation, newDelegation, _getDelegatorVotingPower(delegator)); } + /** + * @dev Transfers voting assets. + */ + function _transferVotingAssets( + address from, + address to, + uint256 amount + ) internal virtual { + if (from == address(0)) { + _totalCheckpoints.push(_add, amount); + } + if (to == address(0)) { + _totalCheckpoints.push(_subtract, amount); + } + _moveVotingPower(delegates(from), delegates(to), amount); + } + /** * @dev Moves voting power. */ @@ -137,17 +157,13 @@ abstract contract Votes is IVotes, Context, EIP712 { address from, address to, uint256 amount - ) internal virtual { + ) private { if (from != to && amount > 0) { - if (from == address(0)) { - _totalCheckpoints.push(_add, amount); - } else { + if (from != address(0)) { (uint256 oldValue, uint256 newValue) = _delegateCheckpoints[from].push(_subtract, amount); emit DelegateVotesChanged(from, oldValue, newValue); } - if (to == address(0)) { - _totalCheckpoints.push(_subtract, amount); - } else { + if (to != address(0)) { (uint256 oldValue, uint256 newValue) = _delegateCheckpoints[to].push(_add, amount); emit DelegateVotesChanged(to, oldValue, newValue); } @@ -169,9 +185,9 @@ abstract contract Votes is IVotes, Context, EIP712 { } /** - * @dev "Consume a nonce": return the current value and increment. + * @dev Consumes a nonce. * - * _Available since v4.1._ + * Returns the current value and increments nonce. */ function _useNonce(address owner) internal virtual returns (uint256 current) { Counters.Counter storage nonce = _nonces[owner]; diff --git a/contracts/mocks/VotingMock.sol b/contracts/mocks/VotesMock.sol similarity index 76% rename from contracts/mocks/VotingMock.sol rename to contracts/mocks/VotesMock.sol index d689a2d1afa..d7d73b929f8 100644 --- a/contracts/mocks/VotingMock.sol +++ b/contracts/mocks/VotesMock.sol @@ -7,8 +7,8 @@ import "../governance/utils/Votes.sol"; contract VotesMock is Votes { constructor(string memory name) EIP712(name, "1") {} - function getTotalVotes() public view returns (uint256) { - return _getTotalVotes(); + function getTotalSupply() public view returns (uint256) { + return _getTotalSupply(); } function delegate(address account, address newDelegation) public { @@ -20,6 +20,6 @@ contract VotesMock is Votes { } function giveVotingPower(address account, uint8 amount) external { - _moveVotingPower(address(0), account, amount); + _transferVotingAssets(address(0), account, amount); } } diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index f49d622ba93..dafd4eb7be9 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -7,16 +7,15 @@ import "../ERC721.sol"; import "../../../governance/utils/Votes.sol"; /** - * @dev Extension of ERC721 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. + * @dev Extension of ERC721 to support voting and delegation, where each individual NFT counts as 1 vote. * * 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 {getVotes} and {getPastVotes}. + * 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 `getVotes` and `getPastVotes`. * * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. - * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this + * Enabling self-delegation can easily be done by overriding the `delegates` function. Keep in mind however that this * will significantly increase the base gas cost of transfers. * * _Available since v4.5._ @@ -25,15 +24,15 @@ abstract contract ERC721Votes is ERC721, Votes { /** * @dev Move voting power when tokens are transferred. * - * Emits a {DelegateVotesChanged} event. + * Emits a {Votes-DelegateVotesChanged} event. */ function _afterTokenTransfer( address from, address to, uint256 tokenId ) internal virtual override { + _transferVotingAssets(from, to, 1); super._afterTokenTransfer(from, to, tokenId); - _moveVotingPower(delegates(from), delegates(to), 1); } /** diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index 782d93d2e6a..6dd401cfe08 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -5,7 +5,13 @@ import "./math/Math.sol"; import "./math/SafeCast.sol"; /** - * @dev Checkpoints operations. + * @dev This library defines the `History` struct, for checkpointing values as they change at different points in + * time, and later looking up past values by block number. See {Votes}. + * + * To create a history of checkpoints define a variable type `Checkpoints.History` in your contract, and store new + * checkpoints or the current transaction block using the {push} function. + * + * _Available since v4.5._ */ library Checkpoints { struct Checkpoint { @@ -45,7 +51,10 @@ library Checkpoints { } /** - * @dev Creates checkpoint + * @dev Creates checkpoint if History is `empty`, or if the current `blockNumber` is higher than the latest + * position `_blockNumber`, otherwise will change the stored value for current block. + * + * Returns previous value and current value. */ function push(History storage self, uint256 value) internal returns (uint256, uint256) { uint256 pos = self._checkpoints.length; @@ -61,7 +70,7 @@ library Checkpoints { } /** - * @dev Creates checkpoint + * @dev Creates checkpoint using the given `op` to compute `value`. */ function push( History storage self, diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 4edcf923bb1..78f44b61858 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -86,6 +86,8 @@ Note that, in all cases, accounts simply _declare_ their interfaces, but they ar {{EnumerableSet}} +{{Checkpoints}} + == Libraries {{Create2}} diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 0fba214476a..9f04ab5974e 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -18,10 +18,6 @@ OpenZeppelin’s Governor system was designed with a concern for compatibility w The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. -=== ERC721Votes - -The ERC721 extension to keep track of votes and vote delegation is one such case. - === Governor & GovernorCompatibilityBravo An OpenZeppelin Governor contract is by default not interface-compatible with GovernorAlpha or Bravo, since some of the functions are different or missing, although it shares all of the same events. However, it’s possible to opt in to full compatibility by inheriting from the GovernorCompatibilityBravo module. The contract will be cheaper to deploy and use without this module. @@ -123,39 +119,7 @@ contract MyToken is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper { } ``` -If your project requires The voting power of each account in our governance setup will be determined by an ERC721 token. The token has to implement the ERC721Votes extension. This extension will keep track of historical balances so that voting power is retrieved from past snapshots rather than current balance, which is an important protection that prevents double voting. - -```solidity -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "openzeppelin/contracts/token/ERC721/extensions/draft-ERC721Votes.sol"; - -contract MyToken is ERC721Votes { - constructor() ERC721("MyToken", "MTK") EIP712("MyToken", "1") {} - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer( - address from, - address to, - uint256 tokenId - ) internal override(ERC721Votes) { - super._afterTokenTransfer(from, to, tokenId); - } - - function _mint(address to, uint256 tokenId) internal override(ERC721Votes) { - super._mint(to, tokenId); - } - - function _burn(uint256 tokenId) internal override(ERC721Votes) { - super._burn(tokenId); - } -} -``` - -NOTE: Voting power could be determined in different ways: multiple ERC20 tokens, ERC721 tokens, sybil resistant identities, etc. All of these options are potentially supported by writing a custom Votes module for your Governor. +NOTE: Voting power could be determined in different ways: multiple ERC20 tokens, ERC721 tokens, sybil resistant identities, etc. All of these options are potentially supported by writing a custom Votes module for your Governor. The only other source of voting power available in OpenZeppelin Contracts currently is xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`]. === Governor diff --git a/test/utils/Voting.test.js b/test/governance/utils/Votes.test.js similarity index 83% rename from test/utils/Voting.test.js rename to test/governance/utils/Votes.test.js index 4110c0f85b4..464dbe379a7 100644 --- a/test/utils/Voting.test.js +++ b/test/governance/utils/Votes.test.js @@ -11,7 +11,7 @@ contract('Voting', function (accounts) { }); it('starts with zero votes', async function () { - expect(await this.voting.getTotalVotes()).to.be.bignumber.equal('0'); + expect(await this.voting.getTotalSupply()).to.be.bignumber.equal('0'); }); describe('move voting power', function () { @@ -34,8 +34,8 @@ contract('Voting', function (accounts) { expect(await this.voting.delegates(account3)).to.be.equal(account2); }); - it('returns amount of votes for account', async function () { - expect(await this.voting.getVotes(account1)).to.be.bignumber.equal('1'); + it('returns total amount of votes', async function () { + expect(await this.voting.getTotalSupply()).to.be.bignumber.equal('3'); }); }); }); diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 917c08f683c..f7ed14018e2 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -453,7 +453,7 @@ contract('ERC721Votes', function (accounts) { it('returns the same total supply on transfers', async function () { await this.token.delegate(holder, { from: holder }); - await this.token.delegate(recipient, { from: recipient }); + //await this.token.delegate(recipient, { from: recipient }); const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); From c60d9b5f17f1f6087517b2e814eb5b74888e1d5c Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Mon, 6 Dec 2021 19:13:42 -0300 Subject: [PATCH 280/300] add Checkpoints: prefix to revert reason --- contracts/utils/Checkpoints.sol | 2 +- test/governance/utils/Votes.test.js | 2 +- test/token/ERC721/extensions/ERC721Votes.test.js | 2 +- test/utils/Checkpoints.test.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index 6dd401cfe08..d4f337fb502 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -35,7 +35,7 @@ library Checkpoints { * @dev Returns checkpoints at given block number. */ function getAtBlock(History storage self, uint256 blockNumber) internal view returns (uint256) { - require(blockNumber < block.number, "block not yet mined"); + require(blockNumber < block.number, "Checkpoints: block not yet mined"); uint256 high = self._checkpoints.length; uint256 low = 0; diff --git a/test/governance/utils/Votes.test.js b/test/governance/utils/Votes.test.js index 464dbe379a7..4a4dd1abb01 100644 --- a/test/governance/utils/Votes.test.js +++ b/test/governance/utils/Votes.test.js @@ -24,7 +24,7 @@ contract('Voting', function (accounts) { it('reverts if block number >= current block', async function () { await expectRevert( this.voting.getPastTotalSupply(this.tx3.receipt.blockNumber + 1), - 'block not yet mined', + 'Checkpoints: block not yet mined', ); }); diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index f7ed14018e2..7bee4f08c43 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -369,7 +369,7 @@ contract('ERC721Votes', function (accounts) { it('reverts if block number >= current block', async function () { await expectRevert( this.token.getPastVotes(other1, 5e10), - 'block not yet mined', + 'Checkpoints: block not yet mined', ); }); diff --git a/test/utils/Checkpoints.test.js b/test/utils/Checkpoints.test.js index 1b1d0c43418..c1c0f885509 100644 --- a/test/utils/Checkpoints.test.js +++ b/test/utils/Checkpoints.test.js @@ -25,7 +25,7 @@ contract('Checkpoints', function (accounts) { it('reverts if block number >= current block', async function () { await expectRevert( this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber + 1), - 'block not yet mined', + 'Checkpoints: block not yet mined', ); }); }); From 81972f74959218afaab71acd1ea828331cb308dd Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Mon, 6 Dec 2021 20:17:13 -0300 Subject: [PATCH 281/300] fix revert reason --- contracts/governance/utils/Votes.sol | 2 +- test/governance/utils/Votes.test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index f52d5b279ca..31b6c7c2e5f 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -71,7 +71,7 @@ abstract contract Votes is IVotes, Context, EIP712 { * - `blockNumber` must have been already mined */ function getPastTotalSupply(uint256 blockNumber) public view virtual override returns (uint256) { - require(blockNumber < block.number, "ERC721Votes: block not yet mined"); + require(blockNumber < block.number, "Votes: block not yet finished"); return _totalCheckpoints.getAtBlock(blockNumber); } diff --git a/test/governance/utils/Votes.test.js b/test/governance/utils/Votes.test.js index 4a4dd1abb01..7b50aab5725 100644 --- a/test/governance/utils/Votes.test.js +++ b/test/governance/utils/Votes.test.js @@ -4,7 +4,7 @@ const { expect } = require('chai'); const Votes = artifacts.require('VotesMock'); -contract('Voting', function (accounts) { +contract('Votes', function (accounts) { const [ account1, account2, account3 ] = accounts; beforeEach(async function () { this.voting = await Votes.new('MyVote'); @@ -24,7 +24,7 @@ contract('Voting', function (accounts) { it('reverts if block number >= current block', async function () { await expectRevert( this.voting.getPastTotalSupply(this.tx3.receipt.blockNumber + 1), - 'Checkpoints: block not yet mined', + 'Votes: block not yet finished', ); }); From 4773334aa79dbe79386d2cfa9f13292246253a62 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Mon, 6 Dec 2021 20:18:14 -0300 Subject: [PATCH 282/300] remove revert reason prefix from test --- test/token/ERC721/extensions/ERC721Votes.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 7bee4f08c43..f7ed14018e2 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -369,7 +369,7 @@ contract('ERC721Votes', function (accounts) { it('reverts if block number >= current block', async function () { await expectRevert( this.token.getPastVotes(other1, 5e10), - 'Checkpoints: block not yet mined', + 'block not yet mined', ); }); From 7ead55f09e4ba588e9afa5b6276a6a3d2dc33c4c Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 7 Dec 2021 08:31:30 -0400 Subject: [PATCH 283/300] Update CHANGELOG.md Co-authored-by: Francisco Giordano --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb0b7f28b98..7c9a59ce7cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -* `Voting`: Added a library for vote tracking with delegation. ([#2944](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2944)) +* `Votes`: Added a base contract for vote tracking with delegation. ([#2944](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2944)) * `ERC721Votes`: Added an extension of ERC721 enabled with vote tracking and delegation. ([#2944](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2944)) * `GovernorTimelockControl`: improve the `state()` function to have it reflect cases where a proposal has been canceled directly on the timelock. ([#2977](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2977)) * `Math`: add a `abs(int256)` method that returns the unsigned absolute value of a signed value. ([#2984](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2984)) From f099426c180bf695e8bb5efc2892092045110883 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 7 Dec 2021 09:45:40 -0400 Subject: [PATCH 284/300] Add checkpoints tests --- test/utils/Checkpoints.test.js | 51 +++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/test/utils/Checkpoints.test.js b/test/utils/Checkpoints.test.js index c1c0f885509..b319745e547 100644 --- a/test/utils/Checkpoints.test.js +++ b/test/utils/Checkpoints.test.js @@ -1,4 +1,4 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); +const { expectRevert, time } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); @@ -7,25 +7,44 @@ const CheckpointsImpl = artifacts.require('CheckpointsImpl'); contract('Checkpoints', function (accounts) { beforeEach(async function () { this.checkpoint = await CheckpointsImpl.new(); - this.tx1 = await this.checkpoint.push(1); - this.tx2 = await this.checkpoint.push(2); - this.tx3 = await this.checkpoint.push(3); }); - it('calls latest', async function () { - expect(await this.checkpoint.latest()).to.be.bignumber.equal('3'); + it('calls latest without checkpoints', async function () { + expect(await this.checkpoint.latest()).to.be.bignumber.equal('0'); }); - it('calls getAtBlock', async function () { - expect(await this.checkpoint.getAtBlock(this.tx1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.checkpoint.getAtBlock(this.tx2.receipt.blockNumber - 1)).to.be.bignumber.equal('1'); - expect(await this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber - 1)).to.be.bignumber.equal('2'); - }); + describe.only('with checkpoints', function () { + beforeEach(async function () { + this.tx1 = await this.checkpoint.push(1); + this.tx2 = await this.checkpoint.push(2); + await time.advanceBlock(); + this.tx3 = await this.checkpoint.push(3); + }); + + it('calls latest', async function () { + expect(await this.checkpoint.latest()).to.be.bignumber.equal('3'); + }); + + it('calls getAtBlock', async function () { + expect(await this.checkpoint.getAtBlock(this.tx1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.checkpoint.getAtBlock(this.tx2.receipt.blockNumber - 1)).to.be.bignumber.equal('1'); + expect(await this.checkpoint.getAtBlock(this.tx2.receipt.blockNumber + 1)).to.be.bignumber.equal('2');//Block with no new checkpoints + expect(await this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber - 1)).to.be.bignumber.equal('2'); + await time.advanceBlock(); + await time.advanceBlock(); + expect(await this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); + }); + + it('reverts if block number >= current block', async function () { + await expectRevert( + this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber + 1), + 'Checkpoints: block not yet mined', + ); - it('reverts if block number >= current block', async function () { - await expectRevert( - this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber + 1), - 'Checkpoints: block not yet mined', - ); + await expectRevert( + this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber), + 'Checkpoints: block not yet mined', + ); + }); }); }); From c4d3dfd8904ac668aba6165a07c508cf3b759785 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Tue, 7 Dec 2021 10:06:23 -0400 Subject: [PATCH 285/300] Improve format --- test/utils/Checkpoints.test.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/utils/Checkpoints.test.js b/test/utils/Checkpoints.test.js index b319745e547..cbf913a58a1 100644 --- a/test/utils/Checkpoints.test.js +++ b/test/utils/Checkpoints.test.js @@ -13,7 +13,7 @@ contract('Checkpoints', function (accounts) { expect(await this.checkpoint.latest()).to.be.bignumber.equal('0'); }); - describe.only('with checkpoints', function () { + describe('with checkpoints', function () { beforeEach(async function () { this.tx1 = await this.checkpoint.push(1); this.tx2 = await this.checkpoint.push(2); @@ -28,7 +28,8 @@ contract('Checkpoints', function (accounts) { it('calls getAtBlock', async function () { expect(await this.checkpoint.getAtBlock(this.tx1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); expect(await this.checkpoint.getAtBlock(this.tx2.receipt.blockNumber - 1)).to.be.bignumber.equal('1'); - expect(await this.checkpoint.getAtBlock(this.tx2.receipt.blockNumber + 1)).to.be.bignumber.equal('2');//Block with no new checkpoints + // Block with no new checkpoints + expect(await this.checkpoint.getAtBlock(this.tx2.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); expect(await this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber - 1)).to.be.bignumber.equal('2'); await time.advanceBlock(); await time.advanceBlock(); From 43abcee741c8810f8d01cb01fddd381b1f4852a1 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Tue, 7 Dec 2021 23:42:43 -0300 Subject: [PATCH 286/300] revert back to previous error message --- contracts/governance/utils/Votes.sol | 2 +- test/governance/utils/Votes.test.js | 2 +- test/token/ERC721/extensions/ERC721Votes.test.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index 31b6c7c2e5f..266642df65b 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -71,7 +71,7 @@ abstract contract Votes is IVotes, Context, EIP712 { * - `blockNumber` must have been already mined */ function getPastTotalSupply(uint256 blockNumber) public view virtual override returns (uint256) { - require(blockNumber < block.number, "Votes: block not yet finished"); + require(blockNumber < block.number, "Votes: block not yet mined"); return _totalCheckpoints.getAtBlock(blockNumber); } diff --git a/test/governance/utils/Votes.test.js b/test/governance/utils/Votes.test.js index 7b50aab5725..75cf6facb0d 100644 --- a/test/governance/utils/Votes.test.js +++ b/test/governance/utils/Votes.test.js @@ -24,7 +24,7 @@ contract('Votes', function (accounts) { it('reverts if block number >= current block', async function () { await expectRevert( this.voting.getPastTotalSupply(this.tx3.receipt.blockNumber + 1), - 'Votes: block not yet finished', + 'Votes: block not yet mined', ); }); diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index f7ed14018e2..db8b1c9a95e 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -434,7 +434,7 @@ contract('ERC721Votes', function (accounts) { it('reverts if block number >= current block', async function () { await expectRevert( this.token.getPastTotalSupply(5e10), - 'ERC721Votes: block not yet mined', + 'Votes: block not yet mined', ); }); From cd3ab1764cfbb5b0faef66d2dbf723bf030fe598 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Wed, 8 Dec 2021 00:06:50 -0300 Subject: [PATCH 287/300] restructure Checkpoints tests --- test/utils/Checkpoints.test.js | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/test/utils/Checkpoints.test.js b/test/utils/Checkpoints.test.js index cbf913a58a1..d212efa1378 100644 --- a/test/utils/Checkpoints.test.js +++ b/test/utils/Checkpoints.test.js @@ -9,41 +9,49 @@ contract('Checkpoints', function (accounts) { this.checkpoint = await CheckpointsImpl.new(); }); - it('calls latest without checkpoints', async function () { - expect(await this.checkpoint.latest()).to.be.bignumber.equal('0'); + describe('without checkpoints', function () { + it('returns zero as latest value', async function () { + expect(await this.checkpoint.latest()).to.be.bignumber.equal('0'); + }); + + it('returns zero as past value', async function () { + await time.advanceBlock(); + expect(await this.checkpoint.getAtBlock(await web3.eth.getBlockNumber() - 1)).to.be.bignumber.equal('0'); + }); }); describe('with checkpoints', function () { - beforeEach(async function () { + beforeEach('pushing checkpoints', async function () { this.tx1 = await this.checkpoint.push(1); this.tx2 = await this.checkpoint.push(2); await time.advanceBlock(); this.tx3 = await this.checkpoint.push(3); + await time.advanceBlock(); + await time.advanceBlock(); }); - it('calls latest', async function () { + it('returns latest value', async function () { expect(await this.checkpoint.latest()).to.be.bignumber.equal('3'); }); - it('calls getAtBlock', async function () { + it('returns past values', async function () { expect(await this.checkpoint.getAtBlock(this.tx1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.checkpoint.getAtBlock(this.tx2.receipt.blockNumber - 1)).to.be.bignumber.equal('1'); + expect(await this.checkpoint.getAtBlock(this.tx1.receipt.blockNumber )).to.be.bignumber.equal('1'); + expect(await this.checkpoint.getAtBlock(this.tx2.receipt.blockNumber )).to.be.bignumber.equal('2'); // Block with no new checkpoints expect(await this.checkpoint.getAtBlock(this.tx2.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); - expect(await this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber - 1)).to.be.bignumber.equal('2'); - await time.advanceBlock(); - await time.advanceBlock(); + expect(await this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber )).to.be.bignumber.equal('3'); expect(await this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); }); it('reverts if block number >= current block', async function () { await expectRevert( - this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber + 1), + this.checkpoint.getAtBlock(await web3.eth.getBlockNumber()), 'Checkpoints: block not yet mined', ); await expectRevert( - this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber), + this.checkpoint.getAtBlock(await web3.eth.getBlockNumber() + 1), 'Checkpoints: block not yet mined', ); }); From c349618ea0f0cf59e17385cfe41de685d899fc6e Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Wed, 8 Dec 2021 00:13:52 -0300 Subject: [PATCH 288/300] lint --- test/utils/Checkpoints.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/utils/Checkpoints.test.js b/test/utils/Checkpoints.test.js index d212efa1378..37f9013ecc8 100644 --- a/test/utils/Checkpoints.test.js +++ b/test/utils/Checkpoints.test.js @@ -36,11 +36,11 @@ contract('Checkpoints', function (accounts) { it('returns past values', async function () { expect(await this.checkpoint.getAtBlock(this.tx1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.checkpoint.getAtBlock(this.tx1.receipt.blockNumber )).to.be.bignumber.equal('1'); - expect(await this.checkpoint.getAtBlock(this.tx2.receipt.blockNumber )).to.be.bignumber.equal('2'); + expect(await this.checkpoint.getAtBlock(this.tx1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.checkpoint.getAtBlock(this.tx2.receipt.blockNumber)).to.be.bignumber.equal('2'); // Block with no new checkpoints expect(await this.checkpoint.getAtBlock(this.tx2.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); - expect(await this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber )).to.be.bignumber.equal('3'); + expect(await this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber)).to.be.bignumber.equal('3'); expect(await this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); }); From ebd3082d916f61760d588d1d56c8fb2f8593ddbe Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Wed, 8 Dec 2021 00:18:22 -0300 Subject: [PATCH 289/300] improve Checkpoints docs --- contracts/utils/Checkpoints.sol | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index d4f337fb502..2d31539a6f1 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -6,10 +6,10 @@ import "./math/SafeCast.sol"; /** * @dev This library defines the `History` struct, for checkpointing values as they change at different points in - * time, and later looking up past values by block number. See {Votes}. + * time, and later looking up past values by block number. See {Votes} as an example. * - * To create a history of checkpoints define a variable type `Checkpoints.History` in your contract, and store new - * checkpoints or the current transaction block using the {push} function. + * To create a history of checkpoints define a variable type `Checkpoints.History` in your contract, and store a new + * checkpoint for the current transaction block using the {push} function. * * _Available since v4.5._ */ @@ -24,7 +24,7 @@ library Checkpoints { } /** - * @dev Returns the value in the latest checkpoint. + * @dev Returns the value in the latest checkpoint, or zero if there are no checkpoints. */ function latest(History storage self) internal view returns (uint256) { uint256 pos = self._checkpoints.length; @@ -32,7 +32,8 @@ library Checkpoints { } /** - * @dev Returns checkpoints at given block number. + * @dev Returns the value at a given block number. If a checkpoint is not available at that block, the closest one + * before it is returned, or zero otherwise. */ function getAtBlock(History storage self, uint256 blockNumber) internal view returns (uint256) { require(blockNumber < block.number, "Checkpoints: block not yet mined"); @@ -51,10 +52,9 @@ library Checkpoints { } /** - * @dev Creates checkpoint if History is `empty`, or if the current `blockNumber` is higher than the latest - * position `_blockNumber`, otherwise will change the stored value for current block. + * @dev Pushes a value onto a History so that it is stored as the checkpoint for the current block. * - * Returns previous value and current value. + * Returns previous value and new value. */ function push(History storage self, uint256 value) internal returns (uint256, uint256) { uint256 pos = self._checkpoints.length; @@ -70,7 +70,10 @@ library Checkpoints { } /** - * @dev Creates checkpoint using the given `op` to compute `value`. + * @dev Pushes a value onto a History, by updating the latest value using binary operation `op`. The new value will + * be set to `op(latest, delta)`. + * + * Returns previous value and new value. */ function push( History storage self, From ce1df1f24156bc1ddacc7cc5d9ba48aa8398ecf7 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 8 Dec 2021 08:24:57 -0400 Subject: [PATCH 290/300] Update revert message --- contracts/governance/utils/Votes.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index 31b6c7c2e5f..05cdf5e1025 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -71,7 +71,7 @@ abstract contract Votes is IVotes, Context, EIP712 { * - `blockNumber` must have been already mined */ function getPastTotalSupply(uint256 blockNumber) public view virtual override returns (uint256) { - require(blockNumber < block.number, "Votes: block not yet finished"); + require(blockNumber < block.number, "Votes: block not yet mined"); return _totalCheckpoints.getAtBlock(blockNumber); } @@ -108,14 +108,14 @@ abstract contract Votes is IVotes, Context, EIP712 { bytes32 r, bytes32 s ) public virtual override { - require(block.timestamp <= expiry, "ERC721Votes: signature expired"); + require(block.timestamp <= expiry, "Votes: signature expired"); address signer = ECDSA.recover( _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), v, r, s ); - require(nonce == _useNonce(signer), "ERC721Votes: invalid nonce"); + require(nonce == _useNonce(signer), "Votes: invalid nonce"); _delegate(signer, delegatee); } From 5cb991ba12d3f390a2c3bfcfdefc471968f2c485 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 8 Dec 2021 12:59:15 -0400 Subject: [PATCH 291/300] Add VotesWorklfow to aid testing --- contracts/governance/utils/Votes.sol | 1 - contracts/mocks/ERC721VotesMock.sol | 4 + contracts/mocks/VotesMock.sol | 23 +- test/governance/utils/Votes.test.js | 44 +- .../utils/VotesWorkflow.behavior.js | 341 +++++++++++++ .../ERC721/extensions/ERC721Votes.test.js | 482 ++++-------------- 6 files changed, 486 insertions(+), 409 deletions(-) create mode 100644 test/governance/utils/VotesWorkflow.behavior.js diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index 05cdf5e1025..1b56e208e1e 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -129,7 +129,6 @@ abstract contract Votes is IVotes, Context, EIP712 { _delegation[delegator] = newDelegation; emit DelegateChanged(delegator, oldDelegation, newDelegation); - _moveVotingPower(oldDelegation, newDelegation, _getDelegatorVotingPower(delegator)); } diff --git a/contracts/mocks/ERC721VotesMock.sol b/contracts/mocks/ERC721VotesMock.sol index 6331d8331bd..0755ace66ce 100644 --- a/contracts/mocks/ERC721VotesMock.sol +++ b/contracts/mocks/ERC721VotesMock.sol @@ -7,6 +7,10 @@ import "../token/ERC721/extensions/draft-ERC721Votes.sol"; contract ERC721VotesMock is ERC721Votes { constructor(string memory name, string memory symbol) ERC721(name, symbol) EIP712(name, "1") {} + function getTotalSupply() public view returns (uint256) { + return _getTotalSupply(); + } + function mint(address account, uint256 tokenId) public { _mint(account, tokenId); } diff --git a/contracts/mocks/VotesMock.sol b/contracts/mocks/VotesMock.sol index d7d73b929f8..545a0260376 100644 --- a/contracts/mocks/VotesMock.sol +++ b/contracts/mocks/VotesMock.sol @@ -5,6 +5,9 @@ pragma solidity ^0.8.0; import "../governance/utils/Votes.sol"; contract VotesMock is Votes { + mapping(address => uint256) private _balances; + mapping(uint256 => address) private _owners; + constructor(string memory name) EIP712(name, "1") {} function getTotalSupply() public view returns (uint256) { @@ -15,11 +18,23 @@ contract VotesMock is Votes { return _delegate(account, newDelegation); } - function _getDelegatorVotingPower(address) internal virtual override returns (uint256) { - return 1; + function _getDelegatorVotingPower(address account) internal virtual override returns (uint256) { + return _balances[account]; + } + + function mint(address account, uint256 voteId) external { + _balances[account] += 1; + _owners[voteId] = account; + _transferVotingAssets(address(0), account, 1); + } + + function burn(uint256 voteId) external { + address owner = _owners[voteId]; + _balances[owner] -= 1; + _transferVotingAssets(owner, address(0), 1); } - function giveVotingPower(address account, uint8 amount) external { - _transferVotingAssets(address(0), account, amount); + function getChainId() external view returns (uint256) { + return block.chainid; } } diff --git a/test/governance/utils/Votes.test.js b/test/governance/utils/Votes.test.js index 7b50aab5725..4118504fbed 100644 --- a/test/governance/utils/Votes.test.js +++ b/test/governance/utils/Votes.test.js @@ -1,41 +1,61 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); +const { expectRevert, BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { + runVotesWorkflow, +} = require('./VotesWorkflow.behavior'); + const Votes = artifacts.require('VotesMock'); contract('Votes', function (accounts) { const [ account1, account2, account3 ] = accounts; beforeEach(async function () { - this.voting = await Votes.new('MyVote'); + this.name = 'My Vote'; + this.votes = await Votes.new(this.name); }); it('starts with zero votes', async function () { - expect(await this.voting.getTotalSupply()).to.be.bignumber.equal('0'); + expect(await this.votes.getTotalSupply()).to.be.bignumber.equal('0'); }); - describe('move voting power', function () { + describe('performs voting operations', function () { beforeEach(async function () { - this.tx1 = await this.voting.giveVotingPower(account1, 1); - this.tx2 = await this.voting.giveVotingPower(account2, 1); - this.tx3 = await this.voting.giveVotingPower(account3, 1); + this.tx1 = await this.votes.mint(account1, 1); + this.tx2 = await this.votes.mint(account2, 1); + this.tx3 = await this.votes.mint(account3, 1); }); it('reverts if block number >= current block', async function () { await expectRevert( - this.voting.getPastTotalSupply(this.tx3.receipt.blockNumber + 1), - 'Votes: block not yet finished', + this.votes.getPastTotalSupply(this.tx3.receipt.blockNumber + 1), + 'Votes: block not yet mined', ); }); it('delegates', async function () { - await this.voting.delegate(account3, account2); + await this.votes.delegate(account3, account2); - expect(await this.voting.delegates(account3)).to.be.equal(account2); + expect(await this.votes.delegates(account3)).to.be.equal(account2); }); it('returns total amount of votes', async function () { - expect(await this.voting.getTotalSupply()).to.be.bignumber.equal('3'); + expect(await this.votes.getTotalSupply()).to.be.bignumber.equal('3'); + }); + }); + + describe('performs voting workflow', function () { + beforeEach(async function () { + this.chainId = await this.votes.getChainId(); + this.account1 = account1; + this.account2 = account2; + this.account1Delegatee = account2; + this.NFT0 = new BN('10000000000000000000000000'); + this.NFT1 = new BN('10'); + this.NFT2 = new BN('20'); + this.NFT3 = new BN('30'); }); + + runVotesWorkflow(); }); }); diff --git a/test/governance/utils/VotesWorkflow.behavior.js b/test/governance/utils/VotesWorkflow.behavior.js new file mode 100644 index 00000000000..314cf13d4a3 --- /dev/null +++ b/test/governance/utils/VotesWorkflow.behavior.js @@ -0,0 +1,341 @@ +const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); + +const { MAX_UINT256, ZERO_ADDRESS } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const { EIP712Domain, domainSeparator } = require('../../helpers/eip712'); + +const Delegation = [ + { name: 'delegatee', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'expiry', type: 'uint256' }, +]; + +const version = '1'; + +function runVotesWorkflow () { + describe.only("run votes workflow", function () { + it('initial nonce is 0', async function () { + expect(await this.votes.nonces(this.account1)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect( + await this.votes.DOMAIN_SEPARATOR(), + ).to.equal( + await domainSeparator(this.name, version, this.chainId, this.votes.address), + ); + }); + + describe('delegation with signature', function () { + const delegator = Wallet.generate(); + const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); + const nonce = 0; + + const buildData = (chainId, verifyingContract, message) => ({ + data: { + primaryType: 'Delegation', + types: { EIP712Domain, Delegation }, + domain: { name, version, chainId, verifyingContract }, + message, + }, + }); + + beforeEach(async function () { + await this.votes.mint(delegatorAddress, this.NFT0); + }); + + it('accept signed delegation', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.votes.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + expect(await this.votes.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.votes.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + expectEvent(receipt, 'DelegateChanged', { + delegator: delegatorAddress, + fromDelegate: ZERO_ADDRESS, + toDelegate: delegatorAddress, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: delegatorAddress, + previousBalance: '0', + newBalance: '1', + }); + + expect(await this.votes.delegates(delegatorAddress)).to.be.equal(delegatorAddress); + + expect(await this.votes.getVotes(delegatorAddress)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.votes.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1'); + }); + + it('rejects reused signature', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.votes.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + await this.votes.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + + await expectRevert( + this.votes.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), + 'Votes: invalid nonce', + ); + }); + + it('rejects bad delegatee', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.votes.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + + const { logs } = await this.votes.delegateBySig(this.account1Delegatee, nonce, MAX_UINT256, v, r, s); + const { args } = logs.find(({ event }) => event === 'DelegateChanged'); + expect(args.delegator).to.not.be.equal(delegatorAddress); + expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); + expect(args.toDelegate).to.be.equal(this.account1Delegatee); + }); + + it('rejects bad nonce', async function () { + const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.votes.address, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }), + )); + await expectRevert( + this.votes.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 } = fromRpcSig(ethSigUtil.signTypedMessage( + delegator.getPrivateKey(), + buildData(this.chainId, this.votes.address, { + delegatee: delegatorAddress, + nonce, + expiry, + }), + )); + + await expectRevert( + this.votes.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), + 'Votes: signature expired', + ); + }); + }); + + describe('set delegation', function () { + describe('call', function () { + it('delegation with tokens', async function () { + expect(await this.votes.delegates(this.account1)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.votes.delegate(this.account1, { from: this.account1 }); + expectEvent(receipt, 'DelegateChanged', { + delegator: this.account1, + fromDelegate: ZERO_ADDRESS, + toDelegate: this.account1, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: this.account1, + previousBalance: '0', + newBalance: '1', + }); + + expect(await this.votes.delegates(this.account1)).to.be.equal(this.account1); + + expect(await this.votes.getVotes(this.account1)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastVotes(this.account1, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.votes.getPastVotes(this.account1, receipt.blockNumber)).to.be.bignumber.equal('1'); + }); + + it('delegation without tokens', async function () { + expect(await this.votes.delegates(this.account1)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.votes.delegate(this.account1, { from: this.account1 }); + expectEvent(receipt, 'DelegateChanged', { + delegator: this.account1, + fromDelegate: ZERO_ADDRESS, + toDelegate: this.account1, + }); + expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + + expect(await this.votes.delegates(this.account1)).to.be.equal(this.account1); + }); + }); + }); + + describe('change delegation', function () { + beforeEach(async function () { + await this.votes.delegate(this.account1, { from: this.account1 }); + }); + + it('call', async function () { + expect(await this.votes.delegates(this.account1)).to.be.equal(this.account1); + + const { receipt } = await this.votes.delegate(this.account1Delegatee, { from: this.account1 }); + expectEvent(receipt, 'DelegateChanged', { + delegator: this.account1, + fromDelegate: this.account1, + toDelegate: this.account1Delegatee, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: this.account1, + previousBalance: '1', + newBalance: '0', + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: this.account1Delegatee, + previousBalance: '0', + newBalance: '1', + }); + + expect(await this.votes.delegates(this.account1)).to.be.equal(this.account1Delegatee); + + expect(await this.votes.getVotes(this.account1)).to.be.bignumber.equal('0'); + expect(await this.votes.getVotes(this.account1Delegatee)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastVotes(this.account1, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastVotes(this.account1Delegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.votes.getPastVotes(this.account1, receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastVotes(this.account1Delegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); + }); + }); + + describe('getPastTotalSupply', function () { + beforeEach(async function () { + await this.votes.delegate(this.account1, { from: this.account1 }); + }); + + it('reverts if block number >= current block', async function () { + await expectRevert( + this.votes.getPastTotalSupply(5e10), + 'block not yet mined', + ); + }); + + it('returns 0 if there are no checkpoints', async function () { + expect(await this.votes.getPastTotalSupply(0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + const t1 = await this.votes.mint(this.account1, this.NFT0); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.votes.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const t2 = await this.votes.mint(this.account1, this.NFT1); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.votes.getPastTotalSupply(t2.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.votes.mint(this.account1, this.NFT1); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.votes.burn(this.NFT1); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.votes.mint(this.account1, this.NFT2); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.votes.burn(this.NFT2); + await time.advanceBlock(); + await time.advanceBlock(); + const t5 = await this.votes.mint(this.account1, this.NFT3); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.votes.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + }); + }); + + // 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.votes.mint(this.account1, this.NFT0); + await this.votes.mint(this.account1, this.NFT1); + await this.votes.mint(this.account1, this.NFT2); + await this.votes.mint(this.account1, this.NFT3); + }); + + describe('getPastVotes', function () { + it('reverts if block number >= current block', async function () { + await expectRevert( + this.votes.getPastVotes(this.account2, 5e10), + 'block not yet mined', + ); + }); + + it('returns 0 if there are no checkpoints', async function () { + expect(await this.votes.getPastVotes(this.account2, 0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + const t1 = await this.votes.delegate(this.account2, { from: this.account1 }); + await time.advanceBlock(); + await time.advanceBlock(); + const latest = await this.votes.getVotes(this.account2); + expect(await this.votes.getPastVotes(this.account2, t1.receipt.blockNumber)).to.be.bignumber.equal(latest); + expect(await this.votes.getPastVotes(this.account2, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(latest); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const t1 = await this.votes.delegate(this.account2, { from: this.account1 }); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.votes.getPastVotes(this.account2, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + }); + }); + }); + }); +} + +module.exports = { + runVotesWorkflow, +}; diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index f7ed14018e2..0dd6a2ae5f6 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -1,25 +1,15 @@ /* eslint-disable */ -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { BN, expectEvent, time } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); -const { MAX_UINT256, ZERO_ADDRESS, ZERO_BYTES32 } = constants; - -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; const { promisify } = require('util'); const queue = promisify(setImmediate); const ERC721VotesMock = artifacts.require('ERC721VotesMock'); -const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); +const { runVotesWorkflow } = require('../../../governance/utils/VotesWorkflow.behavior'); -const Delegation = [ - { name: 'delegatee', type: 'address' }, - { name: 'nonce', type: 'uint256' }, - { name: 'expiry', type: 'uint256' }, -]; async function countPendingTransactions() { return parseInt( @@ -53,455 +43,163 @@ async function batchInBlock (txs) { } contract('ERC721Votes', function (accounts) { - const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts; - const NFT0 = new BN('10000000000000000000000000'); - const NFT1 = new BN('10'); - const NFT2 = new BN('20'); - const NFT3 = new BN('30'); - const NFT4 = new BN('40'); - const name = 'My Token'; + const [ account1, account2, account1Delegatee, other1, other2 ] = accounts; + this.name = 'My Vote'; const symbol = 'MTKN'; - const version = '1'; beforeEach(async function () { - this.token = await ERC721VotesMock.new(name, symbol); + this.votes = await ERC721VotesMock.new(name, symbol); // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id // from within the EVM as from the JSON RPC interface. // See https://github.com/trufflesuite/ganache-core/issues/515 - this.chainId = await this.token.getChainId(); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); - }); + this.chainId = await this.votes.getChainId(); - it('domain separator', async function () { - expect( - await this.token.DOMAIN_SEPARATOR(), - ).to.equal( - await domainSeparator(name, version, this.chainId, this.token.address), - ); + this.NFT0 = new BN('10000000000000000000000000'); + this.NFT1 = new BN('10'); + this.NFT2 = new BN('20'); + this.NFT3 = new BN('30'); }); - describe('set delegation', function () { - describe('call', function () { - it('delegation with tokens', async function () { - await this.token.mint(holder, NFT0); - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegate(holder, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: '0', - newBalance: '1', - }); - - expect(await this.token.delegates(holder)).to.be.equal(holder); - - expect(await this.token.getVotes(holder)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('1'); - }); - - it('delegation without tokens', async function () { - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegate(holder, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); - - expect(await this.token.delegates(holder)).to.be.equal(holder); - }); - }); - - describe('with signature', function () { - const delegator = Wallet.generate(); - const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); - const nonce = 0; - - const buildData = (chainId, verifyingContract, message) => ({ data: { - primaryType: 'Delegation', - types: { EIP712Domain, Delegation }, - domain: { name, version, chainId, verifyingContract }, - message, - }}); - - beforeEach(async function () { - await this.token.mint(delegatorAddress, NFT0); - }); - - it('accept signed delegation', async function () { - const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - )); - - expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - expectEvent(receipt, 'DelegateChanged', { - delegator: delegatorAddress, - fromDelegate: ZERO_ADDRESS, - toDelegate: delegatorAddress, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: delegatorAddress, - previousBalance: '0', - newBalance: '1', - }); - - expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); - - expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1'); - }); - - it('rejects reused signature', async function () { - const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - )); - - await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), - 'ERC721Votes: invalid nonce', - ); - }); - - it('rejects bad delegatee', async function () { - const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - )); - - const { logs } = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); - const { args } = logs.find(({ event }) => event == 'DelegateChanged'); - expect(args.delegator).to.not.be.equal(delegatorAddress); - expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); - expect(args.toDelegate).to.be.equal(holderDelegatee); - }); - - it('rejects bad nonce', async function () { - const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - )); - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), - 'ERC721Votes: invalid nonce', - ); - }); - - it('rejects expired permit', async function () { - const expiry = (await time.latest()) - time.duration.weeks(1); - const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry, - }), - )); - - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), - 'ERC721Votes: signature expired', - ); - }); - }); - }); - - describe('change delegation', function () { + describe('balanceOf', function () { beforeEach(async function () { - await this.token.mint(holder, NFT0); - await this.token.delegate(holder, { from: holder }); + await this.votes.mint(account1, this.NFT0); + await this.votes.mint(account1, this.NFT1); + await this.votes.mint(account1, this.NFT2); + await this.votes.mint(account1, this.NFT3); }); - it('call', async function () { - expect(await this.token.delegates(holder)).to.be.equal(holder); - - const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: holder, - toDelegate: holderDelegatee, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: '1', - newBalance: '0', - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holderDelegatee, - previousBalance: '0', - newBalance: '1', - }); - - expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); - - expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); - expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); + it('grants to initial account', async function () { + expect(await this.votes.balanceOf(this.account1)).to.be.bignumber.equal('4'); }); }); describe('transfers', function () { beforeEach(async function () { - await this.token.mint(holder, NFT0); + await this.votes.mint(account1, this.NFT0); + await this.votes.mint(account1, this.NFT1); + await this.votes.mint(account1, this.NFT2); + await this.votes.mint(account1, this.NFT3); }); it('no delegation', async function () { - const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); + const { receipt } = await this.votes.transferFrom(account1, account2, this.NFT0, { from: account1 }); + expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: this.NFT0 }); expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); - this.holderVotes = '0'; - this.recipientVotes = '0'; + this.account1Votes = '0'; + this.account2Votes = '0'; }); it('sender delegation', async function () { - await this.token.delegate(holder, { from: holder }); + await this.votes.delegate(account1, { from: account1 }); - const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0' }); + const { receipt } = await this.votes.transferFrom(account1, account2, this.NFT0, { from: account1 }); + expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: this.NFT0 }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: account1, previousBalance: '1', newBalance: '0' }); 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 = '0'; + this.account1Votes = '0'; + this.account2Votes = '0'; }); it('receiver delegation', async function () { - await this.token.delegate(recipient, { from: recipient }); + await this.votes.delegate(account2, { from: account2 }); - const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); + const { receipt } = await this.votes.transferFrom(account1, account2, this.NFT0, { from: account1 }); + expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: this.NFT0 }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: account2, 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'; + this.account1Votes = '0'; + this.account2Votes = '1'; }); it('full delegation', async function () { - await this.token.delegate(holder, { from: holder }); - await this.token.delegate(recipient, { from: recipient }); + await this.votes.delegate(account1, { from: account1 }); + await this.votes.delegate(account2, { from: account2 }); - const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, tokenId: NFT0 }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: holder, previousBalance: '1', newBalance: '0'}); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); + const { receipt } = await this.votes.transferFrom(account1, account2, this.NFT0, { from: account1 }); + expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: this.NFT0 }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: account1, previousBalance: '1', newBalance: '0'}); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: account2, 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'; - }); - - afterEach(async function () { - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); - - // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" - const blockNumber = await time.latestBlock(); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getPastVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes); - }); - }); - - // 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, NFT0); - await this.token.mint(holder, NFT1); - await this.token.mint(holder, NFT2); - await this.token.mint(holder, NFT3); - }); - - describe('balanceOf', function () { - it('grants to initial account', async function () { - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('4'); - }); - }); - - describe('getPastVotes', function () { - it('reverts if block number >= current block', async function () { - await expectRevert( - this.token.getPastVotes(other1, 5e10), - 'block not yet mined', - ); - }); - - it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0'); - }); - - it('returns the latest block if >= last checkpoint block', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('4'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); - }); - - it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal('4'); - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const total = await this.token.balanceOf(holder); - - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.transferFrom(holder, other2, NFT1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.transferFrom(holder, other2, NFT2, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.transferFrom(other2, holder, NFT2, { from: other2 }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal(total); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(total); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('3'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('2'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); - }); - }); - }); - - describe('getPastTotalSupply', function () { - beforeEach(async function () { - t1 = await this.token.mint(holder, NFT0); - await this.token.delegate(holder, { from: holder }); - }); - - it('reverts if block number >= current block', async function () { - await expectRevert( - this.token.getPastTotalSupply(5e10), - 'ERC721Votes: block not yet mined', - ); - }); - - it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); - }); - - it('returns the latest block if >= last checkpoint block', async function () { - - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + this.account1Votes = '0'; + this.account2Votes = '1'; }); it('returns the same total supply on transfers', async function () { - await this.token.delegate(holder, { from: holder }); - //await this.token.delegate(recipient, { from: recipient }); + await this.votes.delegate(account1, { from: account1 }); - const { receipt } = await this.token.transferFrom(holder, recipient, NFT0, { from: holder }); + const { receipt } = await this.votes.transferFrom(account1, account2, this.NFT0, { from: account1 }); await time.advanceBlock(); await time.advanceBlock(); - expect(await this.token.getPastTotalSupply(receipt.blockNumber - 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - }); - - it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const t2 = await this.token.mint(holder, NFT1); - await time.advanceBlock(); - await time.advanceBlock(); + expect(await this.votes.getPastTotalSupply(receipt.blockNumber - 1)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastTotalSupply(receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber - 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); + this.account1Votes = '0'; + this.account2Votes = '0'; }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.mint(holder, NFT1); - await time.advanceBlock(); + const total = await this.votes.balanceOf(account1); + + const t1 = await this.votes.delegate(other1, { from: account1 }); await time.advanceBlock(); - const t2 = await this.token.burn(NFT1); await time.advanceBlock(); + const t2 = await this.votes.transferFrom(account1, other2, this.NFT0, { from: account1 }); await time.advanceBlock(); - const t3 = await this.token.mint(holder, NFT2); await time.advanceBlock(); + const t3 = await this.votes.transferFrom(account1, other2, this.NFT2, { from: account1 }); await time.advanceBlock(); - const t4 = await this.token.burn(NFT2); await time.advanceBlock(); + const t4 = await this.votes.transferFrom(other2, account1, this.NFT2, { from: other2 }); await time.advanceBlock(); - const t5 = await this.token.mint(holder, NFT3); await time.advanceBlock(); + + expect(await this.votes.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal(total); + expect(await this.votes.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(total); + expect(await this.votes.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('3'); + expect(await this.votes.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); + expect(await this.votes.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect(await this.votes.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); + expect(await this.votes.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3'); + expect(await this.votes.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); + + this.account1Votes = '0'; + this.account2Votes = '0'; + }); + + afterEach(async function () { + expect(await this.votes.getVotes(account1)).to.be.bignumber.equal(this.account1Votes); + expect(await this.votes.getVotes(account2)).to.be.bignumber.equal(this.account2Votes); + + // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" + const blockNumber = await time.latestBlock(); await time.advanceBlock(); + expect(await this.votes.getPastVotes(account1, blockNumber)).to.be.bignumber.equal(this.account1Votes); + expect(await this.votes.getPastVotes(account2, blockNumber)).to.be.bignumber.equal(this.account2Votes); + }); + }); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('2'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('2'); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('2'); - expect(await this.token.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); + describe('Voting workflow', function () { + beforeEach(async function () { + this.account1 = account1; + this.account1Delegatee = account1Delegatee; + this.account2 = account2; + this.name = 'My Vote'; }); + + runVotesWorkflow(); }); }); From 37c9e739d6b9d34f1f6e4628954898e1ee53c84f Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 8 Dec 2021 13:28:09 -0400 Subject: [PATCH 292/300] Add mint before delegate on votes tests --- test/governance/utils/VotesWorkflow.behavior.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/governance/utils/VotesWorkflow.behavior.js b/test/governance/utils/VotesWorkflow.behavior.js index 314cf13d4a3..d0db853ce5f 100644 --- a/test/governance/utils/VotesWorkflow.behavior.js +++ b/test/governance/utils/VotesWorkflow.behavior.js @@ -17,7 +17,7 @@ const Delegation = [ const version = '1'; function runVotesWorkflow () { - describe.only("run votes workflow", function () { + describe('run votes workflow', function () { it('initial nonce is 0', async function () { expect(await this.votes.nonces(this.account1)).to.be.bignumber.equal('0'); }); @@ -151,6 +151,7 @@ function runVotesWorkflow () { describe('set delegation', function () { describe('call', function () { it('delegation with tokens', async function () { + await this.votes.mint(this.account1, this.NFT0); expect(await this.votes.delegates(this.account1)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.votes.delegate(this.account1, { from: this.account1 }); @@ -191,6 +192,7 @@ function runVotesWorkflow () { describe('change delegation', function () { beforeEach(async function () { + await this.votes.mint(this.account1, this.NFT0); await this.votes.delegate(this.account1, { from: this.account1 }); }); @@ -213,13 +215,13 @@ function runVotesWorkflow () { previousBalance: '0', newBalance: '1', }); - + const prevBlock = receipt.blockNumber - 1; expect(await this.votes.delegates(this.account1)).to.be.equal(this.account1Delegatee); expect(await this.votes.getVotes(this.account1)).to.be.bignumber.equal('0'); expect(await this.votes.getVotes(this.account1Delegatee)).to.be.bignumber.equal('1'); expect(await this.votes.getPastVotes(this.account1, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); - expect(await this.votes.getPastVotes(this.account1Delegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastVotes(this.account1Delegatee, prevBlock)).to.be.bignumber.equal('0'); await time.advanceBlock(); expect(await this.votes.getPastVotes(this.account1, receipt.blockNumber)).to.be.bignumber.equal('0'); expect(await this.votes.getPastVotes(this.account1Delegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); @@ -319,8 +321,9 @@ function runVotesWorkflow () { await time.advanceBlock(); await time.advanceBlock(); const latest = await this.votes.getVotes(this.account2); + const nextBlock = t1.receipt.blockNumber + 1; expect(await this.votes.getPastVotes(this.account2, t1.receipt.blockNumber)).to.be.bignumber.equal(latest); - expect(await this.votes.getPastVotes(this.account2, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(latest); + expect(await this.votes.getPastVotes(this.account2, nextBlock)).to.be.bignumber.equal(latest); }); it('returns zero if < first checkpoint block', async function () { From 94d63e06341a193aad82c38ff0366453163f18d2 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Wed, 8 Dec 2021 13:46:19 -0400 Subject: [PATCH 293/300] Update token specific test for ERC721Votes --- test/token/ERC721/extensions/ERC721Votes.test.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 0dd6a2ae5f6..94900dc8e5b 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -70,16 +70,13 @@ contract('ERC721Votes', function (accounts) { }); it('grants to initial account', async function () { - expect(await this.votes.balanceOf(this.account1)).to.be.bignumber.equal('4'); + expect(await this.votes.balanceOf(account1)).to.be.bignumber.equal('4'); }); }); describe('transfers', function () { beforeEach(async function () { await this.votes.mint(account1, this.NFT0); - await this.votes.mint(account1, this.NFT1); - await this.votes.mint(account1, this.NFT2); - await this.votes.mint(account1, this.NFT3); }); it('no delegation', async function () { @@ -151,6 +148,10 @@ contract('ERC721Votes', function (accounts) { }); it('generally returns the voting balance at the appropriate checkpoint', async function () { + await this.votes.mint(account1, this.NFT1); + await this.votes.mint(account1, this.NFT2); + await this.votes.mint(account1, this.NFT3); + const total = await this.votes.balanceOf(account1); const t1 = await this.votes.delegate(other1, { from: account1 }); From 90ca1e26df093e142ffdd6ccaa8c1864b108d783 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Thu, 9 Dec 2021 21:25:39 -0300 Subject: [PATCH 294/300] remove added whitespace --- contracts/governance/README.adoc | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index 0c23257e3a1..58daf56e70d 100644 --- a/contracts/governance/README.adoc +++ b/contracts/governance/README.adoc @@ -94,7 +94,6 @@ In a governance system, the {TimelockController} contract is in charge of introd {{TimelockController}} - [[timelock-terminology]] ==== Terminology From 9d705ceb99c76541977810426d76b6e84fcfd83a Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Thu, 9 Dec 2021 23:30:04 -0300 Subject: [PATCH 295/300] further improve docs and rename for new concept of vote units --- contracts/governance/utils/IVotes.sol | 33 ++++--- contracts/governance/utils/Votes.sol | 96 +++++++++---------- contracts/mocks/VotesMock.sol | 6 +- .../token/ERC20/extensions/ERC20Votes.sol | 10 -- .../ERC721/extensions/draft-ERC721Votes.sol | 24 ++--- 5 files changed, 79 insertions(+), 90 deletions(-) diff --git a/contracts/governance/utils/IVotes.sol b/contracts/governance/utils/IVotes.sol index 9bb3daab448..bc3f0d9344b 100644 --- a/contracts/governance/utils/IVotes.sol +++ b/contracts/governance/utils/IVotes.sol @@ -3,43 +3,52 @@ pragma solidity ^0.8.0; /** - * @dev Interface of the Votes standard. + * @dev Common interface for {ERC20Votes}, {ERC721Votes}, and other {Votes}-enabled contracts. * - * _Available since v4.4._ + * _Available since v4.5._ */ interface IVotes { /** - * @dev Returns total amount of votes for account. + * @dev Emitted when an account changes their delegate. + */ + event DelegateChanged(address indexed account, address indexed fromDelegate, address indexed toDelegate); + + /** + * @dev Emitted when a token transfer or delegate change results in changes to a delegate's number of votes. + */ + event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); + + /** + * @dev Returns the current amount of votes that `account` has. */ function getVotes(address account) external view returns (uint256); /** - * @dev Returns total amount of votes at given blockNumber. + * @dev Returns the amount of votes that `account` had at the end of a past block (`blockNumber`). */ function getPastVotes(address account, uint256 blockNumber) external view returns (uint256); /** - * @dev Retrieve the `totalVotingPower` at the end of `blockNumber`. Note, this value is the sum of all balances. - * It is but NOT the sum of all the delegated votes! - * - * Requirements: + * @dev Returns the total supply of votes available at the end of a past block (`blockNumber`). * - * - `blockNumber` must have been already mined + * NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. + * Votes that have not been delegated are still part of total supply, even though they would not participate in a + * vote. */ function getPastTotalSupply(uint256 blockNumber) external view returns (uint256); /** - * @dev Returns account delegation. + * @dev Returns the delegate that `account` has chosen. */ function delegates(address account) external view returns (address); /** - * @dev Delegate votes from the sender to `delegatee`. + * @dev Delegates votes from the sender to `delegatee`. */ function delegate(address delegatee) external; /** - * @dev Delegates votes from signer to `delegatee` + * @dev Delegates votes from signer to `delegatee`. */ function delegateBySig( address delegatee, diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index 1b56e208e1e..89eaf175285 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -8,20 +8,22 @@ import "../../utils/cryptography/draft-EIP712.sol"; import "./IVotes.sol"; /** - * @dev This is a base abstract contract that tracks voting power for a set of accounts with a vote delegation system. - * It can be combined with a token contract to represent voting power as the token unit, see {ERC721Votes}. + * @dev This is a base abstract contract that tracks voting units, which are a measure of voting power that can be + * transferred, and provides a system of vote delegation, where an account can delegate its voting units to a sort of + * "representative" that will pool delegated voting units from different accounts and can then use it to vote in + * decisions. In fact, voting units _must_ be delegated in order to count as actual votes, and an account has to + * delegate those votes to itself if it wishes to participate in decisions and does not have a trusted representative. * - * 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 {getVotes}. + * This contract is often combined with a token contract such that voting units correspond to token units. For an + * example, see {ERC721Votes}. * - * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it - * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. - * Enabling self-delegation can easily be done by overriding the {delegates} function. Keep in mind however that this - * will significantly increase the base gas cost of transfers. + * The full history of delegate votes is tracked on-chain so that governance protocols can consider votes as distributed + * at a particular block number to protect against flash loans and double voting. The opt-in delegate system makes the + * cost of this history tracking optional. * - * When using this module, the derived contract must implement {_getDelegatorVotingPower}, and can use {_transferVotingAssets} - * when a delegator's voting power is changed. + * When using this module the derived contract must implement {_getVotingUnits} (for example, make it return + * {ERC721-balanceOf}), and can use {_transferVotingUnits} to track a change in the distribution of those units (in the + * previous example, it would be included in {ERC721-_beforeTokenTransfer}). * * _Available since v4.5._ */ @@ -39,32 +41,29 @@ abstract contract Votes is IVotes, Context, EIP712 { mapping(address => Counters.Counter) private _nonces; /** - * @dev Emitted when an account changes their delegate. - */ - event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); - - /** - * @dev Emitted when a token transfer or delegate change results in changes to an account's voting power. - */ - event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); - - /** - * @dev Returns total amount of votes for account. + * @dev Returns the current amount of votes that `account` has. */ function getVotes(address account) public view virtual override returns (uint256) { return _delegateCheckpoints[account].latest(); } /** - * @dev Returns total amount of votes at given blockNumber for account. + * @dev Returns the amount of votes that `account` had at the end of a past block (`blockNumber`). + * + * Requirements: + * + * - `blockNumber` must have been already mined */ function getPastVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { return _delegateCheckpoints[account].getAtBlock(blockNumber); } /** - * @dev Retrieve the votes total supply at the end of `blockNumber`. Note, this value is the sum of all balances. - * It is but NOT the sum of all the delegated votes! + * @dev Returns the total supply of votes available at the end of a past block (`blockNumber`). + * + * NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. + * Votes that have not been delegated are still part of total supply, even though they would not participate in a + * vote. * * Requirements: * @@ -76,29 +75,29 @@ abstract contract Votes is IVotes, Context, EIP712 { } /** - * @dev Returns total amount of votes. + * @dev Returns the current total supply of votes. */ function _getTotalSupply() internal view virtual returns (uint256) { return _totalCheckpoints.latest(); } /** - * @dev Returns account delegation. + * @dev Returns the delegate that `account` has chosen. */ function delegates(address account) public view virtual override returns (address) { return _delegation[account]; } /** - * @dev Delegate votes from the sender to `delegatee`. + * @dev Delegates votes from the sender to `delegatee`. */ function delegate(address delegatee) public virtual override { - address delegator = _msgSender(); - _delegate(delegator, delegatee); + address account = _msgSender(); + _delegate(account, delegatee); } /** - * @dev Delegates votes from signer to `delegatee` + * @dev Delegates votes from signer to `delegatee`. */ function delegateBySig( address delegatee, @@ -120,22 +119,23 @@ abstract contract Votes is IVotes, Context, EIP712 { } /** - * @dev Change delegation for `delegator` to `delegatee`. + * @dev Delegate all of `account`'s voting units to `delegatee`. * * Emits events {DelegateChanged} and {DelegateVotesChanged}. */ - function _delegate(address delegator, address newDelegation) internal virtual { - address oldDelegation = delegates(delegator); - _delegation[delegator] = newDelegation; + function _delegate(address account, address delegatee) internal virtual { + address oldDelegate = delegates(account); + _delegation[account] = delegatee; - emit DelegateChanged(delegator, oldDelegation, newDelegation); - _moveVotingPower(oldDelegation, newDelegation, _getDelegatorVotingPower(delegator)); + emit DelegateChanged(account, oldDelegate, delegatee); + _moveDelegateVotes(oldDelegate, delegatee, _getVotingUnits(account)); } /** - * @dev Transfers voting assets. + * @dev Transfers, mints, or burns voting units. To register a mint, `from` should be zero. To register a burn, `to` + * should be zero. Total supply of voting units will be adjusted with mints and burns. */ - function _transferVotingAssets( + function _transferVotingUnits( address from, address to, uint256 amount @@ -146,13 +146,13 @@ abstract contract Votes is IVotes, Context, EIP712 { if (to == address(0)) { _totalCheckpoints.push(_subtract, amount); } - _moveVotingPower(delegates(from), delegates(to), amount); + _moveDelegateVotes(delegates(from), delegates(to), amount); } /** - * @dev Moves voting power. + * @dev Moves delegated votes from one delegate to another. */ - function _moveVotingPower( + function _moveDelegateVotes( address from, address to, uint256 amount @@ -169,16 +169,10 @@ abstract contract Votes is IVotes, Context, EIP712 { } } - /** - * @dev Adds two numbers. - */ function _add(uint256 a, uint256 b) private pure returns (uint256) { return a + b; } - /** - * @dev Subtracts two numbers. - */ function _subtract(uint256 a, uint256 b) private pure returns (uint256) { return a - b; } @@ -202,7 +196,7 @@ abstract contract Votes is IVotes, Context, EIP712 { } /** - * @dev Returns DOMAIN_SEPARATOR. + * @dev Returns the contract's {EIP712} domain separator. */ // solhint-disable-next-line func-name-mixedcase function DOMAIN_SEPARATOR() external view returns (bytes32) { @@ -210,7 +204,7 @@ abstract contract Votes is IVotes, Context, EIP712 { } /** - * @dev Returns the balance of the delegator account + * @dev Must return the voting units held by an account. */ - function _getDelegatorVotingPower(address) internal virtual returns (uint256); + function _getVotingUnits(address) internal virtual returns (uint256); } diff --git a/contracts/mocks/VotesMock.sol b/contracts/mocks/VotesMock.sol index 545a0260376..db06ee9a501 100644 --- a/contracts/mocks/VotesMock.sol +++ b/contracts/mocks/VotesMock.sol @@ -18,20 +18,20 @@ contract VotesMock is Votes { return _delegate(account, newDelegation); } - function _getDelegatorVotingPower(address account) internal virtual override returns (uint256) { + function _getVotingUnits(address account) internal virtual override returns (uint256) { return _balances[account]; } function mint(address account, uint256 voteId) external { _balances[account] += 1; _owners[voteId] = account; - _transferVotingAssets(address(0), account, 1); + _transferVotingUnits(address(0), account, 1); } function burn(uint256 voteId) external { address owner = _owners[voteId]; _balances[owner] -= 1; - _transferVotingAssets(owner, address(0), 1); + _transferVotingUnits(owner, address(0), 1); } function getChainId() external view returns (uint256) { diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index a0f8fc64c24..6b0cf8372e1 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -39,16 +39,6 @@ abstract contract ERC20Votes is IVotes, ERC20Permit { mapping(address => Checkpoint[]) private _checkpoints; Checkpoint[] private _totalSupplyCheckpoints; - /** - * @dev Emitted when an account changes their delegate. - */ - event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); - - /** - * @dev Emitted when a token transfer or delegate change results in changes to an account's voting power. - */ - event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); - /** * @dev Get the `pos`-th checkpoint for `account`. */ diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol index dafd4eb7be9..45f1a307a47 100644 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol @@ -7,22 +7,18 @@ import "../ERC721.sol"; import "../../../governance/utils/Votes.sol"; /** - * @dev Extension of ERC721 to support voting and delegation, where each individual NFT counts as 1 vote. + * @dev Extension of ERC721 to support voting and delegation as implemented by {Votes}, where each individual NFT counts + * as 1 vote unit. * - * 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 `getVotes` and `getPastVotes`. - * - * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it - * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. - * Enabling self-delegation can easily be done by overriding the `delegates` function. Keep in mind however that this - * will significantly increase the base gas cost of transfers. + * Tokens do not count as votes until they are delegated, because votes must be tracked which incurs an additional cost + * on every transfer. Token holders can either delegate to a trusted representative who will decide how to make use of + * the votes in governance decisions, or they can delegate to themselves to be their own representative. * * _Available since v4.5._ */ abstract contract ERC721Votes is ERC721, Votes { /** - * @dev Move voting power when tokens are transferred. + * @dev Adjusts votes when tokens are transferred. * * Emits a {Votes-DelegateVotesChanged} event. */ @@ -31,14 +27,14 @@ abstract contract ERC721Votes is ERC721, Votes { address to, uint256 tokenId ) internal virtual override { - _transferVotingAssets(from, to, 1); + _transferVotingUnits(from, to, 1); super._afterTokenTransfer(from, to, tokenId); } /** - * @dev Returns the balance of the delegator account + * @dev Returns the balance of `account`. */ - function _getDelegatorVotingPower(address delegator) internal virtual override returns (uint256) { - return balanceOf(delegator); + function _getVotingUnits(address account) internal virtual override returns (uint256) { + return balanceOf(account); } } From 8e7444629287e1adae9d47d1ffeb8b48abecf257 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Fri, 10 Dec 2021 00:20:13 -0300 Subject: [PATCH 296/300] rename VotesWorkflow to Votes behavior --- .../utils/{VotesWorkflow.behavior.js => Votes.behavior.js} | 4 ++-- test/governance/utils/Votes.test.js | 6 +++--- test/token/ERC721/extensions/ERC721Votes.test.js | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) rename test/governance/utils/{VotesWorkflow.behavior.js => Votes.behavior.js} (99%) diff --git a/test/governance/utils/VotesWorkflow.behavior.js b/test/governance/utils/Votes.behavior.js similarity index 99% rename from test/governance/utils/VotesWorkflow.behavior.js rename to test/governance/utils/Votes.behavior.js index d0db853ce5f..53fdf7f1f13 100644 --- a/test/governance/utils/VotesWorkflow.behavior.js +++ b/test/governance/utils/Votes.behavior.js @@ -16,7 +16,7 @@ const Delegation = [ const version = '1'; -function runVotesWorkflow () { +function shouldBehaveLikeVotes () { describe('run votes workflow', function () { it('initial nonce is 0', async function () { expect(await this.votes.nonces(this.account1)).to.be.bignumber.equal('0'); @@ -340,5 +340,5 @@ function runVotesWorkflow () { } module.exports = { - runVotesWorkflow, + shouldBehaveLikeVotes, }; diff --git a/test/governance/utils/Votes.test.js b/test/governance/utils/Votes.test.js index 4118504fbed..32b7d1dca57 100644 --- a/test/governance/utils/Votes.test.js +++ b/test/governance/utils/Votes.test.js @@ -3,8 +3,8 @@ const { expectRevert, BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { - runVotesWorkflow, -} = require('./VotesWorkflow.behavior'); + shouldBehaveLikeVotes, +} = require('./Votes.behavior'); const Votes = artifacts.require('VotesMock'); @@ -56,6 +56,6 @@ contract('Votes', function (accounts) { this.NFT3 = new BN('30'); }); - runVotesWorkflow(); + shouldBehaveLikeVotes(); }); }); diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 94900dc8e5b..7d8123b6c4a 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -8,7 +8,6 @@ const queue = promisify(setImmediate); const ERC721VotesMock = artifacts.require('ERC721VotesMock'); -const { runVotesWorkflow } = require('../../../governance/utils/VotesWorkflow.behavior'); async function countPendingTransactions() { @@ -41,6 +40,7 @@ async function batchInBlock (txs) { await network.provider.send('evm_setAutomine', [true]); } } +const { shouldBehaveLikeVotes } = require('../../../governance/utils/Votes.behavior'); contract('ERC721Votes', function (accounts) { const [ account1, account2, account1Delegatee, other1, other2 ] = accounts; @@ -201,6 +201,6 @@ contract('ERC721Votes', function (accounts) { this.name = 'My Vote'; }); - runVotesWorkflow(); + shouldBehaveLikeVotes(); }); }); From d6ae07698cc72344bc7a718487f8f1af1348913c Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Fri, 10 Dec 2021 00:20:25 -0300 Subject: [PATCH 297/300] remove unused helpers --- .../ERC721/extensions/ERC721Votes.test.js | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 7d8123b6c4a..6f001f20b4d 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -8,38 +8,6 @@ const queue = promisify(setImmediate); const ERC721VotesMock = artifacts.require('ERC721VotesMock'); - - -async function countPendingTransactions() { - return parseInt( - await network.provider.send('eth_getBlockTransactionCountByNumber', ['pending']) - ); -} - -async function batchInBlock (txs) { - try { - // disable auto-mining - await network.provider.send('evm_setAutomine', [false]); - // send all transactions - const promises = txs.map(fn => fn()); - // wait for node to have all pending transactions - while (txs.length > await countPendingTransactions()) { - await queue(); - } - // mine one block - await network.provider.send('evm_mine'); - // fetch receipts - const receipts = await Promise.all(promises); - // Sanity check, all tx should be in the same block - const minedBlocks = new Set(receipts.map(({ receipt }) => receipt.blockNumber)); - expect(minedBlocks.size).to.equal(1); - - return receipts; - } finally { - // enable auto-mining - await network.provider.send('evm_setAutomine', [true]); - } -} const { shouldBehaveLikeVotes } = require('../../../governance/utils/Votes.behavior'); contract('ERC721Votes', function (accounts) { From 92537f38a8ff478f630e3135180f7f4d63c320e1 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 10 Dec 2021 08:06:18 -0400 Subject: [PATCH 298/300] Rename tests variables based on contract renamed variable --- test/governance/utils/Votes.behavior.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/governance/utils/Votes.behavior.js b/test/governance/utils/Votes.behavior.js index 53fdf7f1f13..a114bd33a12 100644 --- a/test/governance/utils/Votes.behavior.js +++ b/test/governance/utils/Votes.behavior.js @@ -62,7 +62,7 @@ function shouldBehaveLikeVotes () { const { receipt } = await this.votes.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); expectEvent(receipt, 'DelegateChanged', { - delegator: delegatorAddress, + account: delegatorAddress, fromDelegate: ZERO_ADDRESS, toDelegate: delegatorAddress, }); @@ -156,7 +156,7 @@ function shouldBehaveLikeVotes () { const { receipt } = await this.votes.delegate(this.account1, { from: this.account1 }); expectEvent(receipt, 'DelegateChanged', { - delegator: this.account1, + account: this.account1, fromDelegate: ZERO_ADDRESS, toDelegate: this.account1, }); @@ -179,7 +179,7 @@ function shouldBehaveLikeVotes () { const { receipt } = await this.votes.delegate(this.account1, { from: this.account1 }); expectEvent(receipt, 'DelegateChanged', { - delegator: this.account1, + account: this.account1, fromDelegate: ZERO_ADDRESS, toDelegate: this.account1, }); @@ -201,7 +201,7 @@ function shouldBehaveLikeVotes () { const { receipt } = await this.votes.delegate(this.account1Delegatee, { from: this.account1 }); expectEvent(receipt, 'DelegateChanged', { - delegator: this.account1, + account: this.account1, fromDelegate: this.account1, toDelegate: this.account1Delegatee, }); From b306e617582f9efc4d1d8106f4681cf8af86e6c9 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Fri, 10 Dec 2021 18:38:14 -0300 Subject: [PATCH 299/300] fix missing name variable --- test/governance/utils/Votes.behavior.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/governance/utils/Votes.behavior.js b/test/governance/utils/Votes.behavior.js index a114bd33a12..52ea15cc0f0 100644 --- a/test/governance/utils/Votes.behavior.js +++ b/test/governance/utils/Votes.behavior.js @@ -35,7 +35,7 @@ function shouldBehaveLikeVotes () { const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); const nonce = 0; - const buildData = (chainId, verifyingContract, message) => ({ + const buildData = (chainId, verifyingContract, name, message) => ({ data: { primaryType: 'Delegation', types: { EIP712Domain, Delegation }, @@ -51,7 +51,7 @@ function shouldBehaveLikeVotes () { it('accept signed delegation', async function () { const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( delegator.getPrivateKey(), - buildData(this.chainId, this.votes.address, { + buildData(this.chainId, this.votes.address, this.name, { delegatee: delegatorAddress, nonce, expiry: MAX_UINT256, @@ -83,7 +83,7 @@ function shouldBehaveLikeVotes () { it('rejects reused signature', async function () { const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( delegator.getPrivateKey(), - buildData(this.chainId, this.votes.address, { + buildData(this.chainId, this.votes.address, this.name, { delegatee: delegatorAddress, nonce, expiry: MAX_UINT256, @@ -101,7 +101,7 @@ function shouldBehaveLikeVotes () { it('rejects bad delegatee', async function () { const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( delegator.getPrivateKey(), - buildData(this.chainId, this.votes.address, { + buildData(this.chainId, this.votes.address, this.name, { delegatee: delegatorAddress, nonce, expiry: MAX_UINT256, @@ -118,7 +118,7 @@ function shouldBehaveLikeVotes () { it('rejects bad nonce', async function () { const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( delegator.getPrivateKey(), - buildData(this.chainId, this.votes.address, { + buildData(this.chainId, this.votes.address, this.name, { delegatee: delegatorAddress, nonce, expiry: MAX_UINT256, @@ -134,7 +134,7 @@ function shouldBehaveLikeVotes () { const expiry = (await time.latest()) - time.duration.weeks(1); const { v, r, s } = fromRpcSig(ethSigUtil.signTypedMessage( delegator.getPrivateKey(), - buildData(this.chainId, this.votes.address, { + buildData(this.chainId, this.votes.address, this.name, { delegatee: delegatorAddress, nonce, expiry, From 53847b48febd2f6ae971225ac9b49249dd95e2b7 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Fri, 10 Dec 2021 18:40:33 -0300 Subject: [PATCH 300/300] revert event argument change --- contracts/governance/utils/IVotes.sol | 2 +- test/governance/utils/Votes.behavior.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/governance/utils/IVotes.sol b/contracts/governance/utils/IVotes.sol index bc3f0d9344b..d277a610127 100644 --- a/contracts/governance/utils/IVotes.sol +++ b/contracts/governance/utils/IVotes.sol @@ -11,7 +11,7 @@ interface IVotes { /** * @dev Emitted when an account changes their delegate. */ - event DelegateChanged(address indexed account, address indexed fromDelegate, address indexed toDelegate); + event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); /** * @dev Emitted when a token transfer or delegate change results in changes to a delegate's number of votes. diff --git a/test/governance/utils/Votes.behavior.js b/test/governance/utils/Votes.behavior.js index 52ea15cc0f0..17b49eaa9d1 100644 --- a/test/governance/utils/Votes.behavior.js +++ b/test/governance/utils/Votes.behavior.js @@ -62,7 +62,7 @@ function shouldBehaveLikeVotes () { const { receipt } = await this.votes.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); expectEvent(receipt, 'DelegateChanged', { - account: delegatorAddress, + delegator: delegatorAddress, fromDelegate: ZERO_ADDRESS, toDelegate: delegatorAddress, }); @@ -156,7 +156,7 @@ function shouldBehaveLikeVotes () { const { receipt } = await this.votes.delegate(this.account1, { from: this.account1 }); expectEvent(receipt, 'DelegateChanged', { - account: this.account1, + delegator: this.account1, fromDelegate: ZERO_ADDRESS, toDelegate: this.account1, }); @@ -179,7 +179,7 @@ function shouldBehaveLikeVotes () { const { receipt } = await this.votes.delegate(this.account1, { from: this.account1 }); expectEvent(receipt, 'DelegateChanged', { - account: this.account1, + delegator: this.account1, fromDelegate: ZERO_ADDRESS, toDelegate: this.account1, }); @@ -201,7 +201,7 @@ function shouldBehaveLikeVotes () { const { receipt } = await this.votes.delegate(this.account1Delegatee, { from: this.account1 }); expectEvent(receipt, 'DelegateChanged', { - account: this.account1, + delegator: this.account1, fromDelegate: this.account1, toDelegate: this.account1Delegatee, });

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 literal 0 HcmV?d00001 diff --git a/test/governance/GovernorWorkflow.behavior.js b/test/governance/GovernorWorkflow.behavior.js index 402a08358aa..1fb4823389a 100644 --- a/test/governance/GovernorWorkflow.behavior.js +++ b/test/governance/GovernorWorkflow.behavior.js @@ -34,7 +34,7 @@ function runGovernorWorkflow () { } else if (voter.nfts) { for (const nft of voter.nfts) { await this.token.transferFrom(this.settings.tokenHolder, voter.voter, nft, - { from: this.settings.tokenHolder }); + { from: this.settings.tokenHolder }); } } } diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 384299d495b..11a0f382b28 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -22,6 +22,18 @@ contract('GovernorERC721Mock', function (accounts) { const NFT3 = web3.utils.toWei('30'); const NFT4 = web3.utils.toWei('40'); + // Must be the same as in contract + const ProposalState = { + Pending: new BN('0'), + Active: new BN('1'), + Canceled: new BN('2'), + Defeated: new BN('3'), + Succeeded: new BN('4'), + Queued: new BN('5'), + Expired: new BN('6'), + Executed: new BN('7'), + }; + beforeEach(async function () { this.owner = owner; this.token = await Token.new(tokenName, tokenSymbol); @@ -47,7 +59,7 @@ contract('GovernorERC721Mock', function (accounts) { expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); }); - describe.only('voting with ERC721 token', function () { + describe('voting with ERC721 token', function () { beforeEach(async function () { this.settings = { proposal: [ @@ -80,23 +92,25 @@ contract('GovernorERC721Mock', function (accounts) { this.settings.voters.find(({ address }) => address === voter), ); - if (voter == voter2) { + if (voter === voter2) { expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('2'); } else { expect(await this.token.getVotes(voter, vote.blockNumber)).to.be.bignumber.equal('1'); - } + } } await this.mock.proposalVotes(this.id).then(result => { for (const [key, value] of Object.entries(Enums.VoteType)) { expect(result[`${key.toLowerCase()}Votes`]).to.be.bignumber.equal( Object.values(this.settings.voters).filter(({ support }) => support === value).reduce( - (acc, {nfts}) => acc.add(new BN(nfts.length)), + (acc, { nfts }) => acc.add(new BN(nfts.length)), new BN('0'), - ), + ), ); } }); + + expect(await this.mock.state(this.id)).to.be.bignumber.equal(ProposalState.Executed); }); runGovernorWorkflow(); From 0980cb0cb416d883fbf67fdca598d8fae48e1625 Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Thu, 4 Nov 2021 15:36:25 -0400 Subject: [PATCH 047/300] Delete local file --- openzeppelin-solidity-4.3.2.tgz | Bin 199211 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 openzeppelin-solidity-4.3.2.tgz diff --git a/openzeppelin-solidity-4.3.2.tgz b/openzeppelin-solidity-4.3.2.tgz deleted file mode 100644 index 927e56027f696044c87e4fb5500a59541497e9c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 199211 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≪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%0D88Jl0bKxb@g^9a8 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