Skip to content

Commit

Permalink
Refactor consecutive transfer hooks (#3753)
Browse files Browse the repository at this point in the history
(cherry picked from commit 08d5e4a)
Signed-off-by: Hadrien Croubois <hadrien.croubois@gmail.com>
  • Loading branch information
frangio authored and Amxx committed Oct 17, 2022
1 parent 4e18cdc commit 3742c16
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 177 deletions.
13 changes: 12 additions & 1 deletion CHANGELOG.md
Expand Up @@ -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.
Expand All @@ -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

Expand Down
19 changes: 6 additions & 13 deletions contracts/mocks/ERC721ConsecutiveEnumerableMock.unreachable.sol
Expand Up @@ -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);
}
}
28 changes: 6 additions & 22 deletions contracts/mocks/ERC721ConsecutiveMock.sol
Expand Up @@ -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);
}
}

Expand Down
81 changes: 32 additions & 49 deletions contracts/token/ERC721/ERC721.sol
Expand Up @@ -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");
Expand All @@ -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);
}

/**
Expand All @@ -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);
Expand All @@ -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);
}

/**
Expand All @@ -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");
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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 {}
}
16 changes: 9 additions & 7 deletions contracts/token/ERC721/extensions/ERC721Consecutive.sol
Expand Up @@ -91,15 +91,15 @@ 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;
_sequentialOwnership.push(last, uint160(to));
emit ConsecutiveTransfer(first, last, address(0), to);

// hook after
_afterConsecutiveTokenTransfer(address(0), to, first, batchSize);
_afterTokenTransfer(address(0), to, first, batchSize);
}

return first;
Expand All @@ -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) {
Expand Down
53 changes: 11 additions & 42 deletions contracts/token/ERC721/extensions/ERC721Enumerable.sol
Expand Up @@ -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);
Expand All @@ -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
Expand Down
16 changes: 3 additions & 13 deletions contracts/token/ERC721/extensions/ERC721Pausable.sol
Expand Up @@ -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");
}
Expand Down

0 comments on commit 3742c16

Please sign in to comment.