From 7f6c62a2b9f85887e7c1237fcaf3df56457f461c Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Mar 2023 22:50:22 +0000 Subject: [PATCH] Update docs --- CHANGELOG.md | 8 ++ contracts/token/ERC721/ERC721.sol | 25 ++-- .../ERC721/extensions/ERC721Consecutive.sol | 5 + docs/modules/api/pages/token/ERC721.adoc | 43 ++++++- .../ERC721/extensions/ERC721Consecutive.t.sol | 120 ++++++++++++++++++ .../extensions/ERC721Consecutive.test.js | 3 +- 6 files changed, 185 insertions(+), 19 deletions(-) create mode 100644 test/token/ERC721/extensions/ERC721Consecutive.t.sol diff --git a/CHANGELOG.md b/CHANGELOG.md index 936f447e2..747575bbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 4.8.2 + +- `ERC721Consecutive`: Fixed a bug when `_mintConsecutive` is used for batches of size 1 that could lead to balance overflow. Refer to the breaking changes section in the changelog for a note on the behavior of `ERC721._beforeTokenTransfer`. + +### Breaking changes + +- `ERC721`: The internal function `_beforeTokenTransfer` no longer updates balances, which it previously did when `batchSize` was greater than 1. This change has no consequence unless a custom ERC721 extension is explicitly invoking `_beforeTokenTransfer`. Balance updates in extensions must now be done explicitly using `__unsafe_increaseBalance`, with a name that indicates that there is an invariant that has to be manually verified. + ## 4.8.1 (2023-01-13) * `ERC4626`: Use staticcall instead of call when fetching underlying ERC-20 decimals. ([#3943](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3943)) diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 80fc22de0..277653529 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -467,18 +467,9 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { function _beforeTokenTransfer( address from, address to, - uint256, /* firstTokenId */ + uint256 firstTokenId, uint256 batchSize - ) internal virtual { - if (batchSize > 1) { - if (from != address(0)) { - _balances[from] -= batchSize; - } - if (to != address(0)) { - _balances[to] += batchSize; - } - } - } + ) internal virtual {} /** * @dev Hook that is called after any token transfer. This includes minting and burning. If {ERC721Consecutive} is @@ -500,4 +491,16 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { uint256 firstTokenId, uint256 batchSize ) internal virtual {} + + /** + * @dev Unsafe write access to the balances, used by extensions that "mint" tokens using an {ownerOf} override. + * + * WARNING: Anyone calling this MUST ensure that the balances remain consistent with the ownership. The invariant + * being that for any address `a` the value returned by `balanceOf(a)` must be equal to the number of tokens such + * that `ownerOf(tokenId)` is `a`. + */ + // solhint-disable-next-line func-name-mixedcase + function __unsafe_increaseBalance(address account, uint256 amount) internal { + _balances[account] += amount; + } } diff --git a/contracts/token/ERC721/extensions/ERC721Consecutive.sol b/contracts/token/ERC721/extensions/ERC721Consecutive.sol index fc888d528..29048517e 100644 --- a/contracts/token/ERC721/extensions/ERC721Consecutive.sol +++ b/contracts/token/ERC721/extensions/ERC721Consecutive.sol @@ -96,6 +96,11 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { // push an ownership checkpoint & emit event uint96 last = first + batchSize - 1; _sequentialOwnership.push(last, uint160(to)); + + // The invariant required by this function is preserved because the new sequentialOwnership checkpoint + // is attributing ownership of `batchSize` new tokens to account `to`. + __unsafe_increaseBalance(to, batchSize); + emit ConsecutiveTransfer(first, last, address(0), to); // hook after diff --git a/docs/modules/api/pages/token/ERC721.adoc b/docs/modules/api/pages/token/ERC721.adoc index 60502c1a6..9e01dad95 100644 --- a/docs/modules/api/pages/token/ERC721.adoc +++ b/docs/modules/api/pages/token/ERC721.adoc @@ -90,6 +90,7 @@ :xref-ERC721-_requireMinted-uint256-: xref:token/ERC721.adoc#ERC721-_requireMinted-uint256- :xref-ERC721-_beforeTokenTransfer-address-address-uint256-uint256-: xref:token/ERC721.adoc#ERC721-_beforeTokenTransfer-address-address-uint256-uint256- :xref-ERC721-_afterTokenTransfer-address-address-uint256-uint256-: xref:token/ERC721.adoc#ERC721-_afterTokenTransfer-address-address-uint256-uint256- +:xref-ERC721-__unsafe_increaseBalance-address-uint256-: xref:token/ERC721.adoc#ERC721-__unsafe_increaseBalance-address-uint256- :xref-IERC721-Transfer-address-address-uint256-: xref:token/ERC721.adoc#IERC721-Transfer-address-address-uint256- :xref-IERC721-Approval-address-address-uint256-: xref:token/ERC721.adoc#IERC721-Approval-address-address-uint256- :xref-IERC721-ApprovalForAll-address-address-bool-: xref:token/ERC721.adoc#IERC721-ApprovalForAll-address-address-bool- @@ -144,6 +145,7 @@ :xref-ERC721-_setApprovalForAll-address-address-bool-: xref:token/ERC721.adoc#ERC721-_setApprovalForAll-address-address-bool- :xref-ERC721-_requireMinted-uint256-: xref:token/ERC721.adoc#ERC721-_requireMinted-uint256- :xref-ERC721-_afterTokenTransfer-address-address-uint256-uint256-: xref:token/ERC721.adoc#ERC721-_afterTokenTransfer-address-address-uint256-uint256- +:xref-ERC721-__unsafe_increaseBalance-address-uint256-: xref:token/ERC721.adoc#ERC721-__unsafe_increaseBalance-address-uint256- :xref-IERC721-Transfer-address-address-uint256-: xref:token/ERC721.adoc#IERC721-Transfer-address-address-uint256- :xref-IERC721-Approval-address-address-uint256-: xref:token/ERC721.adoc#IERC721-Approval-address-address-uint256- :xref-IERC721-ApprovalForAll-address-address-bool-: xref:token/ERC721.adoc#IERC721-ApprovalForAll-address-address-bool- @@ -192,6 +194,7 @@ :xref-ERC721-_setApprovalForAll-address-address-bool-: xref:token/ERC721.adoc#ERC721-_setApprovalForAll-address-address-bool- :xref-ERC721-_requireMinted-uint256-: xref:token/ERC721.adoc#ERC721-_requireMinted-uint256- :xref-ERC721-_afterTokenTransfer-address-address-uint256-uint256-: xref:token/ERC721.adoc#ERC721-_afterTokenTransfer-address-address-uint256-uint256- +:xref-ERC721-__unsafe_increaseBalance-address-uint256-: xref:token/ERC721.adoc#ERC721-__unsafe_increaseBalance-address-uint256- :xref-Pausable-Paused-address-: xref:security.adoc#Pausable-Paused-address- :xref-Pausable-Unpaused-address-: xref:security.adoc#Pausable-Unpaused-address- :xref-IERC721-Transfer-address-address-uint256-: xref:token/ERC721.adoc#IERC721-Transfer-address-address-uint256- @@ -227,6 +230,7 @@ :xref-ERC721-_requireMinted-uint256-: xref:token/ERC721.adoc#ERC721-_requireMinted-uint256- :xref-ERC721-_beforeTokenTransfer-address-address-uint256-uint256-: xref:token/ERC721.adoc#ERC721-_beforeTokenTransfer-address-address-uint256-uint256- :xref-ERC721-_afterTokenTransfer-address-address-uint256-uint256-: xref:token/ERC721.adoc#ERC721-_afterTokenTransfer-address-address-uint256-uint256- +:xref-ERC721-__unsafe_increaseBalance-address-uint256-: xref:token/ERC721.adoc#ERC721-__unsafe_increaseBalance-address-uint256- :xref-IERC721-Transfer-address-address-uint256-: xref:token/ERC721.adoc#IERC721-Transfer-address-address-uint256- :xref-IERC721-Approval-address-address-uint256-: xref:token/ERC721.adoc#IERC721-Approval-address-address-uint256- :xref-IERC721-ApprovalForAll-address-address-bool-: xref:token/ERC721.adoc#IERC721-ApprovalForAll-address-address-bool- @@ -261,6 +265,7 @@ :xref-ERC721-_setApprovalForAll-address-address-bool-: xref:token/ERC721.adoc#ERC721-_setApprovalForAll-address-address-bool- :xref-ERC721-_requireMinted-uint256-: xref:token/ERC721.adoc#ERC721-_requireMinted-uint256- :xref-ERC721-_beforeTokenTransfer-address-address-uint256-uint256-: xref:token/ERC721.adoc#ERC721-_beforeTokenTransfer-address-address-uint256-uint256- +:xref-ERC721-__unsafe_increaseBalance-address-uint256-: xref:token/ERC721.adoc#ERC721-__unsafe_increaseBalance-address-uint256- :xref-IERC721-Transfer-address-address-uint256-: xref:token/ERC721.adoc#IERC721-Transfer-address-address-uint256- :xref-IERC721-Approval-address-address-uint256-: xref:token/ERC721.adoc#IERC721-Approval-address-address-uint256- :xref-IERC721-ApprovalForAll-address-address-bool-: xref:token/ERC721.adoc#IERC721-ApprovalForAll-address-address-bool- @@ -299,6 +304,7 @@ :xref-ERC721-_requireMinted-uint256-: xref:token/ERC721.adoc#ERC721-_requireMinted-uint256- :xref-ERC721-_beforeTokenTransfer-address-address-uint256-uint256-: xref:token/ERC721.adoc#ERC721-_beforeTokenTransfer-address-address-uint256-uint256- :xref-ERC721-_afterTokenTransfer-address-address-uint256-uint256-: xref:token/ERC721.adoc#ERC721-_afterTokenTransfer-address-address-uint256-uint256- +:xref-ERC721-__unsafe_increaseBalance-address-uint256-: xref:token/ERC721.adoc#ERC721-__unsafe_increaseBalance-address-uint256- :xref-IERC721-Transfer-address-address-uint256-: xref:token/ERC721.adoc#IERC721-Transfer-address-address-uint256- :xref-IERC721-Approval-address-address-uint256-: xref:token/ERC721.adoc#IERC721-Approval-address-address-uint256- :xref-IERC721-ApprovalForAll-address-address-bool-: xref:token/ERC721.adoc#IERC721-ApprovalForAll-address-address-bool- @@ -348,6 +354,7 @@ :xref-ERC721-_setApprovalForAll-address-address-bool-: xref:token/ERC721.adoc#ERC721-_setApprovalForAll-address-address-bool- :xref-ERC721-_requireMinted-uint256-: xref:token/ERC721.adoc#ERC721-_requireMinted-uint256- :xref-ERC721-_beforeTokenTransfer-address-address-uint256-uint256-: xref:token/ERC721.adoc#ERC721-_beforeTokenTransfer-address-address-uint256-uint256- +:xref-ERC721-__unsafe_increaseBalance-address-uint256-: xref:token/ERC721.adoc#ERC721-__unsafe_increaseBalance-address-uint256- :xref-IERC721-Transfer-address-address-uint256-: xref:token/ERC721.adoc#IERC721-Transfer-address-address-uint256- :xref-IERC721-Approval-address-address-uint256-: xref:token/ERC721.adoc#IERC721-Approval-address-address-uint256- :xref-IERC721-ApprovalForAll-address-address-bool-: xref:token/ERC721.adoc#IERC721-ApprovalForAll-address-address-bool- @@ -385,6 +392,7 @@ :xref-ERC721-_requireMinted-uint256-: xref:token/ERC721.adoc#ERC721-_requireMinted-uint256- :xref-ERC721-_beforeTokenTransfer-address-address-uint256-uint256-: xref:token/ERC721.adoc#ERC721-_beforeTokenTransfer-address-address-uint256-uint256- :xref-ERC721-_afterTokenTransfer-address-address-uint256-uint256-: xref:token/ERC721.adoc#ERC721-_afterTokenTransfer-address-address-uint256-uint256- +:xref-ERC721-__unsafe_increaseBalance-address-uint256-: xref:token/ERC721.adoc#ERC721-__unsafe_increaseBalance-address-uint256- :xref-ERC2981-royaltyInfo-uint256-uint256-: xref:token/common.adoc#ERC2981-royaltyInfo-uint256-uint256- :xref-ERC2981-_feeDenominator--: xref:token/common.adoc#ERC2981-_feeDenominator-- :xref-ERC2981-_setDefaultRoyalty-address-uint96-: xref:token/common.adoc#ERC2981-_setDefaultRoyalty-address-uint96- @@ -439,6 +447,7 @@ :xref-ERC721-_setApprovalForAll-address-address-bool-: xref:token/ERC721.adoc#ERC721-_setApprovalForAll-address-address-bool- :xref-ERC721-_requireMinted-uint256-: xref:token/ERC721.adoc#ERC721-_requireMinted-uint256- :xref-ERC721-_afterTokenTransfer-address-address-uint256-uint256-: xref:token/ERC721.adoc#ERC721-_afterTokenTransfer-address-address-uint256-uint256- +:xref-ERC721-__unsafe_increaseBalance-address-uint256-: xref:token/ERC721.adoc#ERC721-__unsafe_increaseBalance-address-uint256- :xref-AccessControlEnumerable-getRoleMember-bytes32-uint256-: xref:access.adoc#AccessControlEnumerable-getRoleMember-bytes32-uint256- :xref-AccessControlEnumerable-getRoleMemberCount-bytes32-: xref:access.adoc#AccessControlEnumerable-getRoleMemberCount-bytes32- :xref-AccessControlEnumerable-_grantRole-bytes32-address-: xref:access.adoc#AccessControlEnumerable-_grantRole-bytes32-address- @@ -881,6 +890,7 @@ Use along with {totalSupply} to enumerate all tokens. :_checkOnERC721Received: pass:normal[xref:#ERC721-_checkOnERC721Received-address-address-uint256-bytes-[`++_checkOnERC721Received++`]] :_beforeTokenTransfer: pass:normal[xref:#ERC721-_beforeTokenTransfer-address-address-uint256-uint256-[`++_beforeTokenTransfer++`]] :_afterTokenTransfer: pass:normal[xref:#ERC721-_afterTokenTransfer-address-address-uint256-uint256-[`++_afterTokenTransfer++`]] +:__unsafe_increaseBalance: pass:normal[xref:#ERC721-__unsafe_increaseBalance-address-uint256-[`++__unsafe_increaseBalance++`]] [.contract] [[ERC721]] @@ -925,8 +935,9 @@ the Metadata extension, but not including the Enumerable extension, which is ava * {xref-ERC721-_approve-address-uint256-}[`++_approve(to, tokenId)++`] * {xref-ERC721-_setApprovalForAll-address-address-bool-}[`++_setApprovalForAll(owner, operator, approved)++`] * {xref-ERC721-_requireMinted-uint256-}[`++_requireMinted(tokenId)++`] -* {xref-ERC721-_beforeTokenTransfer-address-address-uint256-uint256-}[`++_beforeTokenTransfer(from, to, , batchSize)++`] +* {xref-ERC721-_beforeTokenTransfer-address-address-uint256-uint256-}[`++_beforeTokenTransfer(from, to, firstTokenId, batchSize)++`] * {xref-ERC721-_afterTokenTransfer-address-address-uint256-uint256-}[`++_afterTokenTransfer(from, to, firstTokenId, batchSize)++`] +* {xref-ERC721-__unsafe_increaseBalance-address-uint256-}[`++__unsafe_increaseBalance(account, amount)++`] [.contract-subindex-inherited] .IERC721Metadata @@ -1190,7 +1201,7 @@ Reverts if the `tokenId` has not been minted yet. [.contract-item] [[ERC721-_beforeTokenTransfer-address-address-uint256-uint256-]] -==== `[.contract-item-name]#++_beforeTokenTransfer++#++(address from, address to, uint256, uint256 batchSize)++` [.item-kind]#internal# +==== `[.contract-item-name]#++_beforeTokenTransfer++#++(address from, address to, uint256 firstTokenId, uint256 batchSize)++` [.item-kind]#internal# 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. @@ -1222,6 +1233,16 @@ Calling conditions: To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. +[.contract-item] +[[ERC721-__unsafe_increaseBalance-address-uint256-]] +==== `[.contract-item-name]#++__unsafe_increaseBalance++#++(address account, uint256 amount)++` [.item-kind]#internal# + +Unsafe write access to the balances, used by extensions that "mint" tokens using an {ownerOf} override. + +WARNING: Anyone calling this MUST ensure that the balances remain consistent with the ownership. The invariant +being that for any address `a` the value returned by `balanceOf(a)` must be equal to the number of tokens such +that `ownerOf(tokenId)` is `a`. + :_ownedTokens: pass:normal[xref:#ERC721Enumerable-_ownedTokens-mapping-address----mapping-uint256----uint256--[`++_ownedTokens++`]] :_ownedTokensIndex: pass:normal[xref:#ERC721Enumerable-_ownedTokensIndex-mapping-uint256----uint256-[`++_ownedTokensIndex++`]] :_allTokens: pass:normal[xref:#ERC721Enumerable-_allTokens-uint256--[`++_allTokens++`]] @@ -1289,6 +1310,7 @@ account. * {xref-ERC721-_setApprovalForAll-address-address-bool-}[`++_setApprovalForAll(owner, operator, approved)++`] * {xref-ERC721-_requireMinted-uint256-}[`++_requireMinted(tokenId)++`] * {xref-ERC721-_afterTokenTransfer-address-address-uint256-uint256-}[`++_afterTokenTransfer(from, to, firstTokenId, batchSize)++`] +* {xref-ERC721-__unsafe_increaseBalance-address-uint256-}[`++__unsafe_increaseBalance(account, amount)++`] [.contract-subindex-inherited] .IERC721Metadata @@ -1461,6 +1483,7 @@ make the contract unpausable. * {xref-ERC721-_setApprovalForAll-address-address-bool-}[`++_setApprovalForAll(owner, operator, approved)++`] * {xref-ERC721-_requireMinted-uint256-}[`++_requireMinted(tokenId)++`] * {xref-ERC721-_afterTokenTransfer-address-address-uint256-uint256-}[`++_afterTokenTransfer(from, to, firstTokenId, batchSize)++`] +* {xref-ERC721-__unsafe_increaseBalance-address-uint256-}[`++__unsafe_increaseBalance(account, amount)++`] [.contract-subindex-inherited] .IERC721Metadata @@ -1561,8 +1584,9 @@ ERC721 Token that can be burned (destroyed). * {xref-ERC721-_approve-address-uint256-}[`++_approve(to, tokenId)++`] * {xref-ERC721-_setApprovalForAll-address-address-bool-}[`++_setApprovalForAll(owner, operator, approved)++`] * {xref-ERC721-_requireMinted-uint256-}[`++_requireMinted(tokenId)++`] -* {xref-ERC721-_beforeTokenTransfer-address-address-uint256-uint256-}[`++_beforeTokenTransfer(from, to, , batchSize)++`] +* {xref-ERC721-_beforeTokenTransfer-address-address-uint256-uint256-}[`++_beforeTokenTransfer(from, to, firstTokenId, batchSize)++`] * {xref-ERC721-_afterTokenTransfer-address-address-uint256-uint256-}[`++_afterTokenTransfer(from, to, firstTokenId, batchSize)++`] +* {xref-ERC721-__unsafe_increaseBalance-address-uint256-}[`++__unsafe_increaseBalance(account, amount)++`] [.contract-subindex-inherited] .IERC721Metadata @@ -1685,7 +1709,8 @@ _Available since v4.8._ * {xref-ERC721-_approve-address-uint256-}[`++_approve(to, tokenId)++`] * {xref-ERC721-_setApprovalForAll-address-address-bool-}[`++_setApprovalForAll(owner, operator, approved)++`] * {xref-ERC721-_requireMinted-uint256-}[`++_requireMinted(tokenId)++`] -* {xref-ERC721-_beforeTokenTransfer-address-address-uint256-uint256-}[`++_beforeTokenTransfer(from, to, , batchSize)++`] +* {xref-ERC721-_beforeTokenTransfer-address-address-uint256-uint256-}[`++_beforeTokenTransfer(from, to, firstTokenId, batchSize)++`] +* {xref-ERC721-__unsafe_increaseBalance-address-uint256-}[`++__unsafe_increaseBalance(account, amount)++`] [.contract-subindex-inherited] .IERC721Metadata @@ -1833,8 +1858,9 @@ ERC721 token with storage based token URI management. * {xref-ERC721-_approve-address-uint256-}[`++_approve(to, tokenId)++`] * {xref-ERC721-_setApprovalForAll-address-address-bool-}[`++_setApprovalForAll(owner, operator, approved)++`] * {xref-ERC721-_requireMinted-uint256-}[`++_requireMinted(tokenId)++`] -* {xref-ERC721-_beforeTokenTransfer-address-address-uint256-uint256-}[`++_beforeTokenTransfer(from, to, , batchSize)++`] +* {xref-ERC721-_beforeTokenTransfer-address-address-uint256-uint256-}[`++_beforeTokenTransfer(from, to, firstTokenId, batchSize)++`] * {xref-ERC721-_afterTokenTransfer-address-address-uint256-uint256-}[`++_afterTokenTransfer(from, to, firstTokenId, batchSize)++`] +* {xref-ERC721-__unsafe_increaseBalance-address-uint256-}[`++__unsafe_increaseBalance(account, amount)++`] [.contract-subindex-inherited] .IERC721Metadata @@ -1973,7 +1999,8 @@ _Available since v4.5._ * {xref-ERC721-_approve-address-uint256-}[`++_approve(to, tokenId)++`] * {xref-ERC721-_setApprovalForAll-address-address-bool-}[`++_setApprovalForAll(owner, operator, approved)++`] * {xref-ERC721-_requireMinted-uint256-}[`++_requireMinted(tokenId)++`] -* {xref-ERC721-_beforeTokenTransfer-address-address-uint256-uint256-}[`++_beforeTokenTransfer(from, to, , batchSize)++`] +* {xref-ERC721-_beforeTokenTransfer-address-address-uint256-uint256-}[`++_beforeTokenTransfer(from, to, firstTokenId, batchSize)++`] +* {xref-ERC721-__unsafe_increaseBalance-address-uint256-}[`++__unsafe_increaseBalance(account, amount)++`] [.contract-subindex-inherited] .IERC721Metadata @@ -2097,8 +2124,9 @@ _Available since v4.5._ * {xref-ERC721-_approve-address-uint256-}[`++_approve(to, tokenId)++`] * {xref-ERC721-_setApprovalForAll-address-address-bool-}[`++_setApprovalForAll(owner, operator, approved)++`] * {xref-ERC721-_requireMinted-uint256-}[`++_requireMinted(tokenId)++`] -* {xref-ERC721-_beforeTokenTransfer-address-address-uint256-uint256-}[`++_beforeTokenTransfer(from, to, , batchSize)++`] +* {xref-ERC721-_beforeTokenTransfer-address-address-uint256-uint256-}[`++_beforeTokenTransfer(from, to, firstTokenId, batchSize)++`] * {xref-ERC721-_afterTokenTransfer-address-address-uint256-uint256-}[`++_afterTokenTransfer(from, to, firstTokenId, batchSize)++`] +* {xref-ERC721-__unsafe_increaseBalance-address-uint256-}[`++__unsafe_increaseBalance(account, amount)++`] [.contract-subindex-inherited] .IERC721Metadata @@ -2271,6 +2299,7 @@ _Deprecated in favor of https://wizard.openzeppelin.com/[Contracts Wizard]._ * {xref-ERC721-_setApprovalForAll-address-address-bool-}[`++_setApprovalForAll(owner, operator, approved)++`] * {xref-ERC721-_requireMinted-uint256-}[`++_requireMinted(tokenId)++`] * {xref-ERC721-_afterTokenTransfer-address-address-uint256-uint256-}[`++_afterTokenTransfer(from, to, firstTokenId, batchSize)++`] +* {xref-ERC721-__unsafe_increaseBalance-address-uint256-}[`++__unsafe_increaseBalance(account, amount)++`] [.contract-subindex-inherited] .IERC721Metadata diff --git a/test/token/ERC721/extensions/ERC721Consecutive.t.sol b/test/token/ERC721/extensions/ERC721Consecutive.t.sol new file mode 100644 index 000000000..fc6d3c79f --- /dev/null +++ b/test/token/ERC721/extensions/ERC721Consecutive.t.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../../../../contracts/token/ERC721/extensions/ERC721Consecutive.sol"; +import "forge-std/Test.sol"; + +function toSingleton(address account) pure returns (address[] memory) { + address[] memory accounts = new address[](1); + accounts[0] = account; + return accounts; +} + +contract ERC721ConsecutiveTarget is StdUtils, ERC721Consecutive { + uint256 public totalMinted = 0; + + constructor(address[] memory receivers, uint256[] memory batches) ERC721("", "") { + for (uint256 i = 0; i < batches.length; i++) { + address receiver = receivers[i % receivers.length]; + uint96 batchSize = uint96(bound(batches[i], 0, _maxBatchSize())); + _mintConsecutive(receiver, batchSize); + totalMinted += batchSize; + } + } + + function burn(uint256 tokenId) public { + _burn(tokenId); + } +} + +contract ERC721ConsecutiveTest is Test { + function test_balance(address receiver, uint256[] calldata batches) public { + vm.assume(receiver != address(0)); + + ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches); + + assertEq(token.balanceOf(receiver), token.totalMinted()); + } + + function test_ownership(address receiver, uint256[] calldata batches, uint256[2] calldata unboundedTokenId) public { + vm.assume(receiver != address(0)); + + ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches); + + if (token.totalMinted() > 0) { + uint256 validTokenId = bound(unboundedTokenId[0], 0, token.totalMinted() - 1); + assertEq(token.ownerOf(validTokenId), receiver); + } + + uint256 invalidTokenId = bound(unboundedTokenId[1], token.totalMinted(), type(uint256).max); + vm.expectRevert(); + token.ownerOf(invalidTokenId); + } + + function test_burn(address receiver, uint256[] calldata batches, uint256 unboundedTokenId) public { + vm.assume(receiver != address(0)); + + ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches); + + // only test if we minted at least one token + uint256 supply = token.totalMinted(); + vm.assume(supply > 0); + + // burn a token in [0; supply[ + uint256 tokenId = bound(unboundedTokenId, 0, supply - 1); + token.burn(tokenId); + + // balance should have decreased + assertEq(token.balanceOf(receiver), supply - 1); + + // token should be burnt + vm.expectRevert(); + token.ownerOf(tokenId); + } + + function test_transfer( + address[2] calldata accounts, + uint256[2] calldata unboundedBatches, + uint256[2] calldata unboundedTokenId + ) public { + vm.assume(accounts[0] != address(0)); + vm.assume(accounts[1] != address(0)); + vm.assume(accounts[0] != accounts[1]); + + address[] memory receivers = new address[](2); + receivers[0] = accounts[0]; + receivers[1] = accounts[1]; + + // We assume _maxBatchSize is 5000 (the default). This test will break otherwise. + uint256[] memory batches = new uint256[](2); + batches[0] = bound(unboundedBatches[0], 1, 5000); + batches[1] = bound(unboundedBatches[1], 1, 5000); + + ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(receivers, batches); + + uint256 tokenId0 = bound(unboundedTokenId[0], 0, batches[0] - 1); + uint256 tokenId1 = bound(unboundedTokenId[1], 0, batches[1] - 1) + batches[0]; + + assertEq(token.ownerOf(tokenId0), accounts[0]); + assertEq(token.ownerOf(tokenId1), accounts[1]); + assertEq(token.balanceOf(accounts[0]), batches[0]); + assertEq(token.balanceOf(accounts[1]), batches[1]); + + vm.prank(accounts[0]); + token.transferFrom(accounts[0], accounts[1], tokenId0); + + assertEq(token.ownerOf(tokenId0), accounts[1]); + assertEq(token.ownerOf(tokenId1), accounts[1]); + assertEq(token.balanceOf(accounts[0]), batches[0] - 1); + assertEq(token.balanceOf(accounts[1]), batches[1] + 1); + + vm.prank(accounts[1]); + token.transferFrom(accounts[1], accounts[0], tokenId1); + + assertEq(token.ownerOf(tokenId0), accounts[1]); + assertEq(token.ownerOf(tokenId1), accounts[0]); + assertEq(token.balanceOf(accounts[0]), batches[0]); + assertEq(token.balanceOf(accounts[1]), batches[1]); + } +} diff --git a/test/token/ERC721/extensions/ERC721Consecutive.test.js b/test/token/ERC721/extensions/ERC721Consecutive.test.js index 5e480abb8..5e452a801 100644 --- a/test/token/ERC721/extensions/ERC721Consecutive.test.js +++ b/test/token/ERC721/extensions/ERC721Consecutive.test.js @@ -12,7 +12,8 @@ contract('ERC721Consecutive', function (accounts) { const symbol = 'NFT'; const batches = [ { receiver: user1, amount: 0 }, - { receiver: user1, amount: 3 }, + { receiver: user1, amount: 1 }, + { receiver: user1, amount: 2 }, { receiver: user2, amount: 5 }, { receiver: user3, amount: 0 }, { receiver: user1, amount: 7 },