From 3742c16948cd669ceb0d25d298c3dd3874b146fa Mon Sep 17 00:00:00 2001 From: Francisco Date: Mon, 17 Oct 2022 10:09:23 -0500 Subject: [PATCH] Refactor consecutive transfer hooks (#3753) (cherry picked from commit 08d5e4a9b0e79415d346588884955f86ea3f97a6) Signed-off-by: Hadrien Croubois --- CHANGELOG.md | 13 ++- ...1ConsecutiveEnumerableMock.unreachable.sol | 19 ++--- contracts/mocks/ERC721ConsecutiveMock.sol | 28 ++----- contracts/token/ERC721/ERC721.sol | 81 ++++++++----------- .../ERC721/extensions/ERC721Consecutive.sol | 16 ++-- .../ERC721/extensions/ERC721Enumerable.sol | 53 +++--------- .../ERC721/extensions/ERC721Pausable.sol | 16 +--- .../token/ERC721/extensions/ERC721Votes.sol | 24 ++---- .../ERC721PresetMinterPauserAutoId.sol | 14 +--- 9 files changed, 87 insertions(+), 177 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ca9cc5c9dc..b065e8a8821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,17 @@ ### Breaking changes + * `ERC721`: In order to add support for batch minting via `ERC721Consecutive` it was necessary to make a minor breaking change in the internal interface of `ERC721`. Namely, the hooks `_beforeTokenTransfer` and `_afterTokenTransfer` have one additional argument that may need to be added to overrides: + +```diff + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId, ++ uint256 batchSize + ) internal virtual override +``` + * `ERC4626`: Conversion from shares to assets (and vice-versa) in an empty vault used to consider the possible mismatch between the underlying asset's and the vault's decimals. This initial conversion rate is now set to 1-to-1 irrespective of decimals, which are meant for usability purposes only. The vault now uses the assets decimals by default, so off-chain the numbers should appear the same. Developers overriding the vault decimals to a value that does not match the underlying asset may want to override the `_initialConvertToShares` and `_initialConvertToAssets` to replicate the previous behavior. * `TimelockController`: During deployment, the TimelockController used to grant the `TIMELOCK_ADMIN_ROLE` to the deployer and to the timelock itself. The deployer was then expected to renounce this role once configuration of the timelock is over. Failing to renounce that role allows the deployer to change the timelock permissions (but not to bypass the delay for any time-locked actions). The role is no longer given to the deployer by default. A new parameter `admin` can be set to a non-zero address to grant the admin role during construction (to the deployer or any other address). Just like previously, this admin role should be renounced after configuration. If this param is given `address(0)`, the role is not allocated and doesn't need to be revoked. In any case, the timelock itself continues to have this role. @@ -62,7 +73,7 @@ ERC-721 integrators that interpret contract state from events should make sure that they implement the clearing of approval that is implicit in every transfer according to the EIP. Previous versions of OpenZeppelin Contracts emitted an explicit `Approval` event even though it was not required by the specification, and this is no longer the case. -With the new `ERC721Consecutive` extension, the internal workings of `ERC721` are slightly changed. Custom extensions to ERC721 should be reviewed to ensure they remain correct. The new internal functions that should be considered are `_ownerOf`, `_beforeConsecutiveTokenTransfer`, and `_afterConsecutiveTokenTransfer`, and the existing internal functions that should be reviewed are `_exists`, `_beforeTokenTransfer`, and `_afterTokenTransfer`. +With the new `ERC721Consecutive` extension, the internal workings of `ERC721` are slightly changed. Custom extensions to ERC721 should be reviewed to ensure they remain correct. The internal functions that should be considered are `_ownerOf` (new), `_beforeTokenTransfer`, and `_afterTokenTransfer`. ## 4.7.3 diff --git a/contracts/mocks/ERC721ConsecutiveEnumerableMock.unreachable.sol b/contracts/mocks/ERC721ConsecutiveEnumerableMock.unreachable.sol index 791bf3f933f..cde3bd86cff 100644 --- a/contracts/mocks/ERC721ConsecutiveEnumerableMock.unreachable.sol +++ b/contracts/mocks/ERC721ConsecutiveEnumerableMock.unreachable.sol @@ -38,25 +38,18 @@ contract ERC721ConsecutiveEnumerableMock is ERC721Consecutive, ERC721Enumerable function _beforeTokenTransfer( address from, address to, - uint256 tokenId + uint256 firstTokenId, + uint256 batchSize ) internal virtual override(ERC721, ERC721Enumerable) { - super._beforeTokenTransfer(from, to, tokenId); + super._beforeTokenTransfer(from, to, firstTokenId, batchSize); } function _afterTokenTransfer( address from, address to, - uint256 tokenId + uint256 firstTokenId, + uint256 batchSize ) internal virtual override(ERC721, ERC721Consecutive) { - super._afterTokenTransfer(from, to, tokenId); - } - - function _beforeConsecutiveTokenTransfer( - address from, - address to, - uint256 first, - uint96 size - ) internal virtual override(ERC721, ERC721Enumerable) { - super._beforeConsecutiveTokenTransfer(from, to, first, size); + super._afterTokenTransfer(from, to, firstTokenId, batchSize); } } diff --git a/contracts/mocks/ERC721ConsecutiveMock.sol b/contracts/mocks/ERC721ConsecutiveMock.sol index b88bef57893..add5ab160d3 100644 --- a/contracts/mocks/ERC721ConsecutiveMock.sol +++ b/contracts/mocks/ERC721ConsecutiveMock.sol @@ -63,35 +63,19 @@ contract ERC721ConsecutiveMock is ERC721Burnable, ERC721Consecutive, ERC721Pausa function _beforeTokenTransfer( address from, address to, - uint256 tokenId + uint256 firstTokenId, + uint256 batchSize ) internal virtual override(ERC721, ERC721Pausable) { - super._beforeTokenTransfer(from, to, tokenId); + super._beforeTokenTransfer(from, to, firstTokenId, batchSize); } function _afterTokenTransfer( address from, address to, - uint256 tokenId + uint256 firstTokenId, + uint256 batchSize ) internal virtual override(ERC721, ERC721Votes, ERC721Consecutive) { - super._afterTokenTransfer(from, to, tokenId); - } - - function _beforeConsecutiveTokenTransfer( - address from, - address to, - uint256 first, - uint96 size - ) internal virtual override(ERC721, ERC721Pausable) { - super._beforeConsecutiveTokenTransfer(from, to, first, size); - } - - function _afterConsecutiveTokenTransfer( - address from, - address to, - uint256 first, - uint96 size - ) internal virtual override(ERC721, ERC721Votes) { - super._afterConsecutiveTokenTransfer(from, to, first, size); + super._afterTokenTransfer(from, to, firstTokenId, batchSize); } } diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 09a825fbcab..08e03929352 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -287,7 +287,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { require(to != address(0), "ERC721: mint to the zero address"); require(!_exists(tokenId), "ERC721: token already minted"); - _beforeTokenTransfer(address(0), to, tokenId); + _beforeTokenTransfer(address(0), to, tokenId, 1); // Check that tokenId was not minted by `_beforeTokenTransfer` hook require(!_exists(tokenId), "ERC721: token already minted"); @@ -304,7 +304,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { emit Transfer(address(0), to, tokenId); - _afterTokenTransfer(address(0), to, tokenId); + _afterTokenTransfer(address(0), to, tokenId, 1); } /** @@ -321,7 +321,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { function _burn(uint256 tokenId) internal virtual { address owner = ERC721.ownerOf(tokenId); - _beforeTokenTransfer(owner, address(0), tokenId); + _beforeTokenTransfer(owner, address(0), tokenId, 1); // Update ownership in case tokenId was transferred by `_beforeTokenTransfer` hook owner = ERC721.ownerOf(tokenId); @@ -338,7 +338,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { emit Transfer(owner, address(0), tokenId); - _afterTokenTransfer(owner, address(0), tokenId); + _afterTokenTransfer(owner, address(0), tokenId, 1); } /** @@ -360,7 +360,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner"); require(to != address(0), "ERC721: transfer to the zero address"); - _beforeTokenTransfer(from, to, tokenId); + _beforeTokenTransfer(from, to, tokenId, 1); // Check that tokenId was not transferred by `_beforeTokenTransfer` hook require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner"); @@ -381,7 +381,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { emit Transfer(from, to, tokenId); - _afterTokenTransfer(from, to, tokenId); + _afterTokenTransfer(from, to, tokenId, 1); } /** @@ -451,70 +451,53 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { } /** - * @dev Hook that is called before any (single) token transfer. This includes minting and burning. - * See {_beforeConsecutiveTokenTransfer}. + * @dev Hook that is called before any token transfer. This includes minting and burning. If {ERC721Consecutive} is + * used, the hook may be called as part of a consecutive (batch) mint, as indicated by `batchSize` greater than 1. * * Calling conditions: * - * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be - * transferred to `to`. - * - When `from` is zero, `tokenId` will be minted for `to`. - * - When `to` is zero, ``from``'s `tokenId` will be burned. + * - When `from` and `to` are both non-zero, ``from``'s tokens will be transferred to `to`. + * - When `from` is zero, the tokens will be minted for `to`. + * - When `to` is zero, ``from``'s tokens will be burned. * - `from` and `to` are never both zero. + * - `batchSize` is non-zero. * * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */ function _beforeTokenTransfer( address from, address to, - uint256 tokenId - ) internal virtual {} + uint256, /* firstTokenId */ + uint256 batchSize + ) internal virtual { + if (batchSize > 1) { + if (from != address(0)) { + _balances[from] -= batchSize; + } + if (to != address(0)) { + _balances[to] += batchSize; + } + } + } /** - * @dev Hook that is called after any (single) transfer of tokens. This includes minting and burning. - * See {_afterConsecutiveTokenTransfer}. + * @dev Hook that is called after any token transfer. This includes minting and burning. If {ERC721Consecutive} is + * used, the hook may be called as part of a consecutive (batch) mint, as indicated by `batchSize` greater than 1. * * Calling conditions: * - * - when `from` and `to` are both non-zero. + * - When `from` and `to` are both non-zero, ``from``'s tokens were transferred to `to`. + * - When `from` is zero, the tokens were minted for `to`. + * - When `to` is zero, ``from``'s tokens were burned. * - `from` and `to` are never both zero. + * - `batchSize` is non-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 {} - - /** - * @dev Hook that is called before "consecutive token transfers" as defined in ERC2309 and implemented in - * {ERC721Consecutive}. - * Calling conditions are similar to {_beforeTokenTransfer}. - */ - function _beforeConsecutiveTokenTransfer( - address from, - address to, - uint256, /*first*/ - uint96 size - ) internal virtual { - if (from != address(0)) { - _balances[from] -= size; - } - if (to != address(0)) { - _balances[to] += size; - } - } - - /** - * @dev Hook that is called after "consecutive token transfers" as defined in ERC2309 and implemented in - * {ERC721Consecutive}. - * Calling conditions are similar to {_afterTokenTransfer}. - */ - function _afterConsecutiveTokenTransfer( - address, /*from*/ - address, /*to*/ - uint256, /*first*/ - uint96 /*size*/ + uint256 firstTokenId, + uint256 batchSize ) internal virtual {} } diff --git a/contracts/token/ERC721/extensions/ERC721Consecutive.sol b/contracts/token/ERC721/extensions/ERC721Consecutive.sol index f039ac2d6d8..59cfd563be3 100644 --- a/contracts/token/ERC721/extensions/ERC721Consecutive.sol +++ b/contracts/token/ERC721/extensions/ERC721Consecutive.sol @@ -91,7 +91,7 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { require(batchSize <= _maxBatchSize(), "ERC721Consecutive: batch too large"); // hook before - _beforeConsecutiveTokenTransfer(address(0), to, first, batchSize); + _beforeTokenTransfer(address(0), to, first, batchSize); // push an ownership checkpoint & emit event uint96 last = first + batchSize - 1; @@ -99,7 +99,7 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { emit ConsecutiveTransfer(first, last, address(0), to); // hook after - _afterConsecutiveTokenTransfer(address(0), to, first, batchSize); + _afterTokenTransfer(address(0), to, first, batchSize); } return first; @@ -122,16 +122,18 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { function _afterTokenTransfer( address from, address to, - uint256 tokenId + uint256 firstTokenId, + uint256 batchSize ) internal virtual override { if ( to == address(0) && // if we burn - tokenId < _totalConsecutiveSupply() && // and the tokenId was minted in a batch - !_sequentialBurn.get(tokenId) // and the token was never marked as burnt + firstTokenId < _totalConsecutiveSupply() && // and the tokenId was minted in a batch + !_sequentialBurn.get(firstTokenId) // and the token was never marked as burnt ) { - _sequentialBurn.set(tokenId); + require(batchSize == 1, "ERC721Consecutive: batch burn not supported"); + _sequentialBurn.set(firstTokenId); } - super._afterTokenTransfer(from, to, tokenId); + super._afterTokenTransfer(from, to, firstTokenId, batchSize); } function _totalConsecutiveSupply() private view returns (uint96) { diff --git a/contracts/token/ERC721/extensions/ERC721Enumerable.sol b/contracts/token/ERC721/extensions/ERC721Enumerable.sol index 54aaffbc80f..2ffdcb32048 100644 --- a/contracts/token/ERC721/extensions/ERC721Enumerable.sol +++ b/contracts/token/ERC721/extensions/ERC721Enumerable.sol @@ -55,25 +55,22 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { } /** - * @dev Hook that is called before any token transfer. This includes minting - * and burning. - * - * Calling conditions: - * - * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be - * transferred to `to`. - * - When `from` is zero, `tokenId` will be minted for `to`. - * - When `to` is zero, ``from``'s `tokenId` will be burned. - * - `from` and 'to' cannot be the zero address at the same time. - * - * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @dev See {ERC721-_beforeTokenTransfer}. */ function _beforeTokenTransfer( address from, address to, - uint256 tokenId + uint256 firstTokenId, + uint256 batchSize ) internal virtual override { - super._beforeTokenTransfer(from, to, tokenId); + super._beforeTokenTransfer(from, to, firstTokenId, batchSize); + + if (batchSize > 1) { + // Will only trigger during construction. Batch transferring (minting) is not available afterwards. + revert("ERC721Enumerable: consecutive transfers not supported"); + } + + uint256 tokenId = firstTokenId; if (from == address(0)) { _addTokenToAllTokensEnumeration(tokenId); @@ -87,34 +84,6 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { } } - /** - * @dev Hook that is called before any batch token transfer. For now this is limited - * to batch minting by the {ERC721Consecutive} extension. - * - * Calling conditions: - * - * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be - * transferred to `to`. - * - When `from` is zero, `tokenId` will be minted for `to`. - * - When `to` is zero, ``from``'s `tokenId` will be burned. - * - `from` cannot be the zero address. - * - `to` cannot be the zero address. - * - * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. - */ - function _beforeConsecutiveTokenTransfer( - address, - address, - uint256, - uint96 size - ) internal virtual override { - // We revert because enumerability is not supported with consecutive batch minting. - // This conditional is only needed to silence spurious warnings about unreachable code. - if (size > 0) { - revert("ERC721Enumerable: consecutive transfers not supported"); - } - } - /** * @dev Private function to add a token to this extension's ownership-tracking data structures. * @param to address representing the new owner of the given token ID diff --git a/contracts/token/ERC721/extensions/ERC721Pausable.sol b/contracts/token/ERC721/extensions/ERC721Pausable.sol index d5b27625b95..91418817684 100644 --- a/contracts/token/ERC721/extensions/ERC721Pausable.sol +++ b/contracts/token/ERC721/extensions/ERC721Pausable.sol @@ -24,20 +24,10 @@ abstract contract ERC721Pausable is ERC721, Pausable { function _beforeTokenTransfer( address from, address to, - uint256 tokenId + uint256 firstTokenId, + uint256 batchSize ) internal virtual override { - super._beforeTokenTransfer(from, to, tokenId); - - require(!paused(), "ERC721Pausable: token transfer while paused"); - } - - function _beforeConsecutiveTokenTransfer( - address from, - address to, - uint256 first, - uint96 size - ) internal virtual override { - super._beforeConsecutiveTokenTransfer(from, to, first, size); + super._beforeTokenTransfer(from, to, firstTokenId, batchSize); require(!paused(), "ERC721Pausable: token transfer while paused"); } diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 7c81cdf97c1..06c0c2e8527 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -18,32 +18,18 @@ import "../../../governance/utils/Votes.sol"; */ abstract contract ERC721Votes is ERC721, Votes { /** - * @dev Adjusts votes when tokens are transferred. + * @dev See {ERC721-_afterTokenTransfer}. Adjusts votes when tokens are transferred. * * Emits a {IVotes-DelegateVotesChanged} event. */ function _afterTokenTransfer( address from, address to, - uint256 tokenId + uint256 firstTokenId, + uint256 batchSize ) internal virtual override { - _transferVotingUnits(from, to, 1); - super._afterTokenTransfer(from, to, tokenId); - } - - /** - * @dev Adjusts votes when a batch of tokens is transferred. - * - * Emits a {IVotes-DelegateVotesChanged} event. - */ - function _afterConsecutiveTokenTransfer( - address from, - address to, - uint256 first, - uint96 size - ) internal virtual override { - _transferVotingUnits(from, to, size); - super._afterConsecutiveTokenTransfer(from, to, first, size); + _transferVotingUnits(from, to, batchSize); + super._afterTokenTransfer(from, to, firstTokenId, batchSize); } /** diff --git a/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol b/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol index 35ee6c2d615..b016af4d401 100644 --- a/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol +++ b/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol @@ -119,18 +119,10 @@ contract ERC721PresetMinterPauserAutoId is function _beforeTokenTransfer( address from, address to, - uint256 tokenId + uint256 firstTokenId, + uint256 batchSize ) internal virtual override(ERC721, ERC721Enumerable, ERC721Pausable) { - super._beforeTokenTransfer(from, to, tokenId); - } - - function _beforeConsecutiveTokenTransfer( - address from, - address to, - uint256 first, - uint96 size - ) internal virtual override(ERC721, ERC721Enumerable, ERC721Pausable) { - super._beforeConsecutiveTokenTransfer(from, to, first, size); + super._beforeTokenTransfer(from, to, firstTokenId, batchSize); } /**