From 6e4425ced32f63c21f4b90d07e0898459c9cdbd0 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Tue, 17 Jan 2023 21:12:34 +0100 Subject: [PATCH 01/74] added comments on transfer hooks --- contracts/ERC721A.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index d35d97141..2b605c23b 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -649,6 +649,7 @@ contract ERC721A is IERC721A { * @dev Hook that is called before a set of serially-ordered token IDs * are about to be transferred. This includes minting. * And also called before burning one token. + * But not called on batch transfers. * * `startTokenId` - the first token ID to be transferred. * `quantity` - the amount to be transferred. @@ -672,6 +673,7 @@ contract ERC721A is IERC721A { * @dev Hook that is called after a set of serially-ordered token IDs * have been transferred. This includes minting. * And also called after one token has been burned. + * But not called on batch transfers. * * `startTokenId` - the first token ID to be transferred. * `quantity` - the amount to be transferred. From 06f89f32037751bed0948239e429c418563ddc09 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Tue, 17 Jan 2023 21:13:41 +0100 Subject: [PATCH 02/74] added sort --- contracts/ERC721A.sol | 148 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 1 deletion(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 2b605c23b..128383277 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1135,4 +1135,150 @@ contract ERC721A is IERC721A { revert(0x00, 0x04) } } -} \ No newline at end of file + + /** + * @dev Sorts the array in-place with intro-quicksort. + */ + function _sort(uint256[] memory a) internal pure { + /// @solidity memory-safe-assembly + assembly { + let w := not(31) + let s := 0x20 + let n := mload(a) // Length of `a`. + mstore(a, 0) // For insertion sort's inner loop to terminate. + // Let the stack be the start of the free memory. + let stack := mload(0x40) + for {} iszero(lt(n, 2)) {} { + // Push `l` and `h` to the stack. + // The `shl` by 5 is equivalent to multiplying by `0x20`. + let l := add(a, s) + let h := add(a, shl(5, n)) + let j := l + // forgefmt: disable-next-item + for {} iszero(or(eq(j, h), gt(mload(j), mload(add(j, s))))) {} { + j := add(j, s) + } + // If the array is already sorted. + if eq(j, h) { break } + j := h + // forgefmt: disable-next-item + for {} iszero(gt(mload(j), mload(add(j, w)))) {} { + j := add(j, w) // `sub(j, 0x20)`. + } + // If the array is reversed sorted. + if eq(j, l) { + for {} 1 {} { + let t := mload(l) + mstore(l, mload(h)) + mstore(h, t) + h := add(h, w) // `sub(h, 0x20)`. + l := add(l, s) + if iszero(lt(l, h)) { break } + } + break + } + // Push `l` and `h` onto the stack. + mstore(stack, l) + mstore(add(stack, s), h) + stack := add(stack, 0x40) + break + } + for { let stackBottom := mload(0x40) } iszero(eq(stack, stackBottom)) {} { + // Pop `l` and `h` from the stack. + stack := sub(stack, 0x40) + let l := mload(stack) + let h := mload(add(stack, s)) + // Do insertion sort if `h - l <= 0x20 * 12`. + // Threshold is fine-tuned via trial and error. + if iszero(gt(sub(h, l), 0x180)) { + // Hardcode sort the first 2 elements. + let i := add(l, s) + if iszero(lt(mload(l), mload(i))) { + let t := mload(i) + mstore(i, mload(l)) + mstore(l, t) + } + for {} 1 {} { + i := add(i, s) + if gt(i, h) { break } + let k := mload(i) // Key. + let j := add(i, w) // The slot before the current slot. + let v := mload(j) // The value of `j`. + if iszero(gt(v, k)) { continue } + for {} 1 {} { + mstore(add(j, s), v) + j := add(j, w) + v := mload(j) + if iszero(gt(v, k)) { break } + } + mstore(add(j, s), k) + } + continue + } + // Pivot slot is the average of `l` and `h`, + // rounded down to nearest multiple of 0x20. + let p := shl(5, shr(6, add(l, h))) + // Median of 3 with sorting. + { + let e0 := mload(l) + let e2 := mload(h) + let e1 := mload(p) + if iszero(lt(e0, e1)) { + let t := e0 + e0 := e1 + e1 := t + } + if iszero(lt(e0, e2)) { + let t := e0 + e0 := e2 + e2 := t + } + if iszero(lt(e1, e2)) { + let t := e1 + e1 := e2 + e2 := t + } + mstore(p, e1) + mstore(h, e2) + mstore(l, e0) + } + // Hoare's partition. + { + // The value of the pivot slot. + let x := mload(p) + p := h + for { let i := l } 1 {} { + for {} 1 {} { + i := add(i, s) + if iszero(gt(x, mload(i))) { break } + } + let j := p + for {} 1 {} { + j := add(j, w) + if iszero(lt(x, mload(j))) { break } + } + p := j + if iszero(lt(i, p)) { break } + // Swap slots `i` and `p`. + let t := mload(i) + mstore(i, mload(p)) + mstore(p, t) + } + } + // If slice on right of pivot is non-empty, push onto stack. + { + mstore(stack, add(p, s)) + // Skip `mstore(add(stack, 0x20), h)`, as it is already on the stack. + stack := add(stack, shl(6, lt(add(p, s), h))) + } + // If slice on left of pivot is non-empty, push onto stack. + { + mstore(stack, l) + mstore(add(stack, s), p) + stack := add(stack, shl(6, gt(p, l))) + } + } + mstore(a, n) // Restore the length of `a`. + } + } +} From 6d7c64f1aeb726336ec985dc0d0322dd1f71e38d Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Tue, 17 Jan 2023 21:14:14 +0100 Subject: [PATCH 03/74] added clearApprovalsAndEmitTransferEvent --- contracts/ERC721A.sol | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 128383277..01b8623a5 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -723,6 +723,49 @@ contract ERC721A is IERC721A { } } + /** + * @dev Private function to handle clearing approvals and emitting transfer Event for a given `tokenId`. + * Used in `_batchTransferFrom`. + * + * `from` - Previous owner of the given token ID. + * `toMasked` - Target address that will receive the token. + * `tokenId` - Token ID to be transferred. + * `isApprovedForAll_` - Whether the caller is approved for all token IDs. + */ + function _clearApprovalsAndEmitTransferEvent( + address from, + uint256 toMasked, + uint256 tokenId, + bool isApprovedForAll_ + ) private { + (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); + + // The nested ifs save around 20+ gas over a compound boolean condition. + if (!isApprovedForAll_) + if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) + _revert(TransferCallerNotOwnerNorApproved.selector); + + // Clear approvals from the previous owner. + assembly { + if approvedAddress { + // This is equivalent to `delete _tokenApprovals[tokenId]`. + sstore(approvedAddressSlot, 0) + } + } + + assembly { + // Emit the `Transfer` event. + log4( + 0, // Start of data (0, since no data). + 0, // End of data (0, since no data). + _TRANSFER_EVENT_SIGNATURE, // Signature. + from, // `from`. + toMasked, // `to`. + tokenId // `tokenId`. + ) + } + } + // ============================================================= // MINT OPERATIONS // ============================================================= From 7a5a17e2d903935cba9d6fea515fefe7f0ebe3e2 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Tue, 17 Jan 2023 21:14:34 +0100 Subject: [PATCH 04/74] added tokenBatchTransfer hooks --- contracts/ERC721A.sol | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 01b8623a5..6d5c7753f 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -693,6 +693,40 @@ contract ERC721A is IERC721A { uint256 quantity ) internal virtual {} + /** + * @dev Hook that is called before a set of token IDs ordered in ascending order + * are about to be transferred. Only called on batch transfers. + * + * `tokenIds` - the array of tokenIds to be transferred, ordered in ascending order. + * + * Calling conditions: + * + * - `from`'s `tokenIds` will be transferred to `to`. + * - Neither `from` and `to` can be zero. + */ + function _beforeTokenBatchTransfers( + address from, + address to, + uint256[] memory tokenIds + ) internal virtual {} + + /** + * @dev Hook that is called after a set of token IDs ordered in ascending order + * have been transferred. Only called on batch transfers. + * + * `tokenIds` - the array of tokenIds transferred, ordered in ascending order. + * + * Calling conditions: + * + * - `from`'s `tokenIds` have been transferred to `to`. + * - Neither `from` and `to` can be zero. + */ + function _afterTokenBatchTransfers( + address from, + address to, + uint256[] memory tokenIds + ) internal virtual {} + /** * @dev Private function to invoke {IERC721Receiver-onERC721Received} on a target contract. * From b85b741f3031e61c96f31638c515001f7fa4b909 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Tue, 17 Jan 2023 21:15:46 +0100 Subject: [PATCH 05/74] added _batchTransferFrom and safe variants --- contracts/ERC721A.sol | 175 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 6d5c7753f..820a5f9f4 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -645,6 +645,181 @@ contract ERC721A is IERC721A { } } + /** + * @dev Transfers `tokenIds` in batch from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenIds` tokens must be owned by `from`. + * - If the caller is not `from`, it must be approved to move these tokens + * by either {approve} or {setApprovalForAll}. + * + * Emits a {Transfer} event for each transfer. + */ + function _batchTransferFrom( + address from, + address to, + uint256[] memory tokenIds + ) internal virtual { + // Sort `tokenIds` to allow batching consecutive ids into single operations. + _sort(tokenIds); + + // Mask `from` and `to` to the lower 160 bits, in case the upper bits somehow aren't clean. + from = address(uint160(uint256(uint160(from)) & _BITMASK_ADDRESS)); + uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS; + if (toMasked == 0) _revert(TransferToZeroAddress.selector); + + bool isApprovedForAll_ = isApprovedForAll(from, _msgSenderERC721A()); + uint256 totalTokens = tokenIds.length; + uint256 totalTokensLeft; + uint256 startTokenId; + uint256 nextTokenId; + uint256 prevOwnershipPacked; + uint256 nextOwnershipPacked; + uint256 quantity; + + _beforeTokenBatchTransfers(from, to, tokenIds); + + // Underflow of the sender's balance is temporarily possible if the wrong set of token Ids is passed, + // but reverts afterwards when ownership is checked. + // The recipient's balance can't realistically overflow. + // Counter overflow is incredibly unrealistic as `startTokenId` would have to be 2**256. + unchecked { + // We can directly increment and decrement the balances. + _packedAddressData[from] -= totalTokens; + _packedAddressData[to] += totalTokens; + + for (uint256 i; i < totalTokens; ) { + startTokenId = tokenIds[i]; + totalTokensLeft = totalTokens - i; + + // Check ownership of `startTokenId`. + prevOwnershipPacked = _packedOwnershipOf(startTokenId); + if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); + + // Updates: + // - `address` to the next owner. + // - `startTimestamp` to the timestamp of transfering. + // - `burned` to `false`. + // - `nextInitialized` to `true`. + _packedOwnerships[startTokenId] = _packOwnershipData( + to, + _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked) + ); + + // Clear approvals and emit transfer event for `startTokenId`. + _clearApprovalsAndEmitTransferEvent(from, toMasked, startTokenId, isApprovedForAll_); + + // Derive quantity by looping over the next consecutive `totalTokensLeft`. + for (quantity = 1; quantity < totalTokensLeft; ++quantity) { + nextTokenId = startTokenId + quantity; + nextOwnershipPacked = _packedOwnerships[nextTokenId]; + + // If `nextTokenId` is not consecutive, update `nextOwnershipPacked` and break from the loop. + if (tokenIds[i + quantity] != nextTokenId) { + // If `quantity` is 1, we can directly use `prevOwnershipPacked`. + uint256 lastOwnershipPacked = quantity == 1 + ? prevOwnershipPacked + : _packedOwnershipOf(nextTokenId - 1); + // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . + if (lastOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { + // If the next slot's address is zero and not burned (i.e. packed value is zero). + if (_packedOwnerships[nextTokenId] == 0) { + // If the next slot is within bounds. + if (nextTokenId != _currentIndex) { + // Initialize the next slot to maintain correctness for `ownerOf(startTokenId + 1)`. + _packedOwnerships[nextTokenId] = lastOwnershipPacked; + } + } + } + break; + } + + nextOwnershipPacked = _packedOwnerships[nextTokenId]; + // If the next slot's address is zero. + if (nextOwnershipPacked == 0) { + // Revert if the next slot is out of bounds. Cannot be higher than `_currentIndex` since we're + // incrementing in steps of one + if (nextTokenId == _currentIndex) _revert(OwnerQueryForNonexistentToken.selector); + // Otherwise we assume `from` owns `nextTokenId` and move on. + } else { + // Revert if `nextTokenId` is not owned by `from`. + if (address(uint160(nextOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); + // Revert if `nextTokenId` has been burned. + if (nextOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); + + // Updates: + // - `address` to the next owner. + // - `startTimestamp` to the timestamp of transfering. + // - `burned` to `false`. + // - `nextInitialized` to `true`. + _packedOwnerships[nextTokenId] = _packOwnershipData( + to, + _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, nextOwnershipPacked) + ); + } + + // Clear approvals and emit transfer event for `nextTokenId`. + _clearApprovalsAndEmitTransferEvent(from, toMasked, nextTokenId, isApprovedForAll_); + } + + // Skip the next `quantity` tokens. + i += quantity; + } + } + + _afterTokenBatchTransfers(from, to, tokenIds); + } + + /** + * @dev Equivalent to `_safeBatchTransferFrom(from, to, tokenIds, '')`. + */ + function _safeBatchTransferFrom( + address from, + address to, + uint256[] memory tokenIds + ) internal virtual { + _safeBatchTransferFrom(from, to, tokenIds, ''); + } + + /** + * @dev Safely transfers `tokenIds` in batch from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenIds` tokens must be owned by `from`. + * - If the caller is not `from`, it must be approved to move these tokens + * by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement + * {IERC721Receiver-onERC721Received}, which is called for each transferred token. + * + * Emits a {Transfer} event for each transfer. + */ + function _safeBatchTransferFrom( + address from, + address to, + uint256[] memory tokenIds, + bytes memory _data + ) internal virtual { + _batchTransferFrom(from, to, tokenIds); + + uint256 tokenId; + uint256 totalTokens = tokenIds.length; + unchecked { + for (uint256 i; i < totalTokens; ++i) { + tokenId = tokenIds[i]; + if (to.code.length != 0) + if (!_checkContractOnERC721Received(from, to, tokenId, _data)) { + _revert(TransferToNonERC721ReceiverImplementer.selector); + } + } + } + } + /** * @dev Hook that is called before a set of serially-ordered token IDs * are about to be transferred. This includes minting. From c6da1dc195c42b4aebbad9e39f2c9a1506eb7778 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Tue, 17 Jan 2023 21:17:04 +0100 Subject: [PATCH 06/74] added ERC721ABatchTransferable extension and interface --- .../extensions/ERC721ABatchTransferable.sol | 40 ++++++++++++ .../extensions/IERC721ABatchTransferable.sol | 62 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 contracts/extensions/ERC721ABatchTransferable.sol create mode 100644 contracts/extensions/IERC721ABatchTransferable.sol diff --git a/contracts/extensions/ERC721ABatchTransferable.sol b/contracts/extensions/ERC721ABatchTransferable.sol new file mode 100644 index 000000000..8bb56a036 --- /dev/null +++ b/contracts/extensions/ERC721ABatchTransferable.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v4.2.3 +// Creator: Chiru Labs + +pragma solidity ^0.8.4; + +import '../ERC721A.sol'; +import './IERC721ABatchTransferable.sol'; + +/** + * @title ERC721ABatchTransferable. + * + * @dev ERC721A token optimized for batch transfers. + */ +abstract contract ERC721ABatchTransferable is ERC721A, IERC721ABatchTransferable { + function batchTransferFrom( + address from, + address to, + uint256[] memory tokenIds + ) public payable virtual override { + _batchTransferFrom(from, to, tokenIds); + } + + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory tokenIds + ) public payable virtual override { + _safeBatchTransferFrom(from, to, tokenIds); + } + + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory tokenIds, + bytes memory _data + ) public payable virtual override { + _safeBatchTransferFrom(from, to, tokenIds, _data); + } +} diff --git a/contracts/extensions/IERC721ABatchTransferable.sol b/contracts/extensions/IERC721ABatchTransferable.sol new file mode 100644 index 000000000..0b984e16b --- /dev/null +++ b/contracts/extensions/IERC721ABatchTransferable.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v4.2.3 +// Creator: Chiru Labs + +pragma solidity ^0.8.4; + +import '../IERC721A.sol'; + +/** + * @dev Interface of ERC721ABatchTransferable. + */ +interface IERC721ABatchTransferable is IERC721A { + /** + * @dev Transfers `tokenIds` in batch from `from` to `to`. See {ERC721A-_batchTransferFrom}. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenIds` tokens must be owned by `from`. + * - If the caller is not `from`, it must be approved to move these tokens + * by either {approve} or {setApprovalForAll}. + * + * Emits a {Transfer} event for each transfer. + */ + function batchTransferFrom( + address from, + address to, + uint256[] memory tokenIds + ) external payable; + + /** + * @dev Equivalent to `safeBatchTransferFrom(from, to, tokenIds, '')`. + */ + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory tokenIds + ) external payable; + + /** + * @dev Safely transfers `tokenIds` in batch from `from` to `to`. See {ERC721A-_safeBatchTransferFrom}. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenIds` tokens must be owned by `from`. + * - If the caller is not `from`, it must be approved to move these tokens + * by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement + * {IERC721Receiver-onERC721Received}, which is called for each transferred token. + * + * Emits a {Transfer} event for each transfer. + */ + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory tokenIds, + bytes memory _data + ) external payable; +} From 9115cfec5c78b7387222b47fe4f41d90a16d5a0b Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 18 Jan 2023 00:52:44 +0100 Subject: [PATCH 07/74] formatting --- contracts/ERC721A.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 820a5f9f4..793c5ba3f 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -699,7 +699,7 @@ contract ERC721A is IERC721A { prevOwnershipPacked = _packedOwnershipOf(startTokenId); if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); - // Updates: + // Updates startTokenId: // - `address` to the next owner. // - `startTimestamp` to the timestamp of transfering. // - `burned` to `false`. @@ -720,6 +720,7 @@ contract ERC721A is IERC721A { // If `nextTokenId` is not consecutive, update `nextOwnershipPacked` and break from the loop. if (tokenIds[i + quantity] != nextTokenId) { // If `quantity` is 1, we can directly use `prevOwnershipPacked`. + // Otherwise we cannot assume _packedOwnershipOf(nextTokenId - 1) == prevOwnershipPacked. uint256 lastOwnershipPacked = quantity == 1 ? prevOwnershipPacked : _packedOwnershipOf(nextTokenId - 1); @@ -729,7 +730,7 @@ contract ERC721A is IERC721A { if (_packedOwnerships[nextTokenId] == 0) { // If the next slot is within bounds. if (nextTokenId != _currentIndex) { - // Initialize the next slot to maintain correctness for `ownerOf(startTokenId + 1)`. + // Initialize the next slot to maintain correctness for `ownerOf(nextTokenId)`. _packedOwnerships[nextTokenId] = lastOwnershipPacked; } } @@ -738,7 +739,8 @@ contract ERC721A is IERC721A { } nextOwnershipPacked = _packedOwnerships[nextTokenId]; - // If the next slot's address is zero. + + // If the next slot's address is uninitialized. if (nextOwnershipPacked == 0) { // Revert if the next slot is out of bounds. Cannot be higher than `_currentIndex` since we're // incrementing in steps of one @@ -750,7 +752,7 @@ contract ERC721A is IERC721A { // Revert if `nextTokenId` has been burned. if (nextOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); - // Updates: + // Updates nextTokenId: // - `address` to the next owner. // - `startTimestamp` to the timestamp of transfering. // - `burned` to `false`. From 571f7e177fc8d917f8d629ce991c3e80d033d8e8 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 18 Jan 2023 00:53:15 +0100 Subject: [PATCH 08/74] added interface and ERC721ABatchTransferableMock --- .../interfaces/IERC721ABatchTransferable.sol | 7 +++ .../mocks/ERC721ABatchTransferableMock.sol | 53 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 contracts/interfaces/IERC721ABatchTransferable.sol create mode 100644 contracts/mocks/ERC721ABatchTransferableMock.sol diff --git a/contracts/interfaces/IERC721ABatchTransferable.sol b/contracts/interfaces/IERC721ABatchTransferable.sol new file mode 100644 index 000000000..bde71ccae --- /dev/null +++ b/contracts/interfaces/IERC721ABatchTransferable.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v4.2.3 +// Creator: Chiru Labs + +pragma solidity ^0.8.4; + +import '../extensions/IERC721ABatchTransferable.sol'; diff --git a/contracts/mocks/ERC721ABatchTransferableMock.sol b/contracts/mocks/ERC721ABatchTransferableMock.sol new file mode 100644 index 000000000..1aa081d46 --- /dev/null +++ b/contracts/mocks/ERC721ABatchTransferableMock.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v4.2.3 +// Creators: Chiru Labs + +pragma solidity ^0.8.4; + +import '../extensions/ERC721ABatchTransferable.sol'; + +contract ERC721ABatchTransferableMock is ERC721ABatchTransferable { + constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {} + + function exists(uint256 tokenId) public view returns (bool) { + return _exists(tokenId); + } + + function safeMint(address to, uint256 quantity) public { + _safeMint(to, quantity); + } + + function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) { + return _ownershipAt(index); + } + + function totalMinted() public view returns (uint256) { + return _totalMinted(); + } + + function totalBurned() public view returns (uint256) { + return _totalBurned(); + } + + function numberBurned(address owner) public view returns (uint256) { + return _numberBurned(owner); + } + + function burn(uint256 tokenId) public { + _burn(tokenId, true); + } + + function batchTransferFromUnoptimized( + address from, + address to, + uint256[] memory tokenIds + ) public { + unchecked { + uint256 tokenId; + for (uint256 i; i < tokenIds.length; ++i) { + tokenId = tokenIds[i]; + transferFrom(from, to, tokenId); + } + } + } +} From c26ce33b5862fdc12c8a097c2ce9c13a81b46b46 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 18 Jan 2023 00:53:42 +0100 Subject: [PATCH 09/74] added ERC721ABatchTransferable tests (wip) --- .../ERC721ABatchTransferable.test.js | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 test/extensions/ERC721ABatchTransferable.test.js diff --git a/test/extensions/ERC721ABatchTransferable.test.js b/test/extensions/ERC721ABatchTransferable.test.js new file mode 100644 index 000000000..97b015007 --- /dev/null +++ b/test/extensions/ERC721ABatchTransferable.test.js @@ -0,0 +1,143 @@ +const { deployContract, getBlockTimestamp, mineBlockTimestamp, offsettedIndex } = require('../helpers.js'); +const { expect } = require('chai'); +const { constants } = require('@openzeppelin/test-helpers'); +const { ZERO_ADDRESS } = constants; + +const createTestSuite = ({ contract, constructorArgs }) => + function () { + let offsetted; + + context(`${contract}`, function () { + beforeEach(async function () { + this.erc721aBatchTransferable = await deployContract(contract, constructorArgs); + + this.startTokenId = this.erc721aBatchTransferable.startTokenId + ? (await this.erc721aBatchTransferable.startTokenId()).toNumber() + : 0; + + offsetted = (...arr) => offsettedIndex(this.startTokenId, arr); + offsetted(0); + }); + + beforeEach(async function () { + const [owner, addr1, addr2, addr3, addr4] = await ethers.getSigners(); + this.owner = owner; + this.addr1 = addr1; + this.addr2 = addr2; + this.addr3 = addr3; + this.addr4 = addr4; + this.numTotalTokens = 20; + await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr2.address, 2); + await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr1.address, 1); + await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr2.address, 1); + await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr1.address, 2); + await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr2.address, 14); + + this.addr1.expected = { + mintCount: 3, + tokens: offsetted(2, 4, 5), + }; + + this.addr2.expected = { + mintCount: 17, + tokens: offsetted(0, 17, 1, 6, 7, 13, 19, 10, 12, 11, 8, 14, 15, 16, 3, 18, 9), + }; + }); + + context('test batch transfer functionality', function () { + const testSuccessfulBatchTransfer = function (transferFn, transferToContract = true) { + beforeEach(async function () { + const sender = this.addr2; + this.tokenIds = this.addr2.expected.tokens; + this.from = sender.address; + this.to = transferToContract ? this.addr3 : this.addr4; + await this.erc721aBatchTransferable.connect(sender).approve(this.to.address, this.tokenIds[0]); + + const ownershipBefore = await this.erc721aBatchTransferable.getOwnershipAt(this.tokenIds[0]); + this.timestampBefore = parseInt(ownershipBefore.startTimestamp); + this.timestampToMine = (await getBlockTimestamp()) + 12345; + await mineBlockTimestamp(this.timestampToMine); + this.timestampMined = await getBlockTimestamp(); + + // prettier-ignore + this.transferTx = await this.erc721aBatchTransferable + .connect(sender)[transferFn](this.from, this.to.address, this.tokenIds); + + const ownershipAfter = await this.erc721aBatchTransferable.getOwnershipAt(this.tokenIds[0]); + this.timestampAfter = parseInt(ownershipAfter.startTimestamp); + }); + + it('transfers the ownership of the given token IDs to the given address', async function () { + for (let i = 0; i < this.tokenIds.length; i++) { + const tokenId = this.tokenIds[i]; + expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.to.address); + } + }); + + it('emits Transfers event', async function () { + for (let i = 0; i < this.tokenIds.length; i++) { + const tokenId = this.tokenIds[i]; + await expect(this.transferTx) + .to.emit(this.erc721aBatchTransferable, 'Transfer') + .withArgs(this.from, this.to.address, tokenId); + } + }); + + it('clears the approval for the token ID', async function () { + expect(await this.erc721aBatchTransferable.getApproved(this.tokenIds[0])).to.be.equal(ZERO_ADDRESS); + }); + + it('adjusts owners balances', async function () { + expect(await this.erc721aBatchTransferable.balanceOf(this.from)).to.be.equal(0); + expect(await this.erc721aBatchTransferable.balanceOf(this.to.address)).to.be.equal( + this.addr2.expected.mintCount + ); + }); + + it('startTimestamp updated correctly', async function () { + expect(this.timestampBefore).to.be.lt(this.timestampToMine); + expect(this.timestampAfter).to.be.gte(this.timestampToMine); + expect(this.timestampAfter).to.be.lt(this.timestampToMine + 10); + expect(this.timestampToMine).to.be.eq(this.timestampMined); + }); + }; + + context('successful transfers', function () { + context('batchTransferFrom', function () { + describe('to contract', function () { + testSuccessfulBatchTransfer('batchTransferFrom'); + }); + + describe('to EOA', function () { + testSuccessfulBatchTransfer('batchTransferFrom', false); + }); + }); + context('safeBatchTransferFrom', function () { + describe('to contract', function () { + testSuccessfulBatchTransfer('safeBatchTransferFrom(address,address,uint256[])'); + }); + + describe('to EOA', function () { + testSuccessfulBatchTransfer('safeBatchTransferFrom(address,address,uint256[])', false); + }); + }); + + // TEMPORARY: to use as comparison for gas usage + context('batchTransferFromUnoptimized', function () { + describe('to contract', function () { + testSuccessfulBatchTransfer('batchTransferFromUnoptimized'); + }); + + describe('to EOA', function () { + testSuccessfulBatchTransfer('batchTransferFromUnoptimized', false); + }); + }); + }); + }); + }); + }; + +describe( + 'ERC721ABatchTransferable', + createTestSuite({ contract: 'ERC721ABatchTransferableMock', constructorArgs: ['Azuki', 'AZUKI'] }) +); From 05c1b6f353bd8cde5b5fe2c9e47df0223d9cd022 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 18 Jan 2023 14:13:36 +0100 Subject: [PATCH 10/74] added approvalCheck --- contracts/ERC721A.sol | 67 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 793c5ba3f..92cb557b2 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -645,6 +645,17 @@ contract ERC721A is IERC721A { } } + /** + * @dev Equivalent to `_batchTransferFrom(from, to, tokenIds, false)`. + */ + function _batchTransferFrom( + address from, + address to, + uint256[] memory tokenIds + ) internal virtual { + _batchTransferFrom(from, to, tokenIds, false); + } + /** * @dev Transfers `tokenIds` in batch from `from` to `to`. * @@ -661,7 +672,8 @@ contract ERC721A is IERC721A { function _batchTransferFrom( address from, address to, - uint256[] memory tokenIds + uint256[] memory tokenIds, + bool approvalCheck ) internal virtual { // Sort `tokenIds` to allow batching consecutive ids into single operations. _sort(tokenIds); @@ -671,7 +683,6 @@ contract ERC721A is IERC721A { uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS; if (toMasked == 0) _revert(TransferToZeroAddress.selector); - bool isApprovedForAll_ = isApprovedForAll(from, _msgSenderERC721A()); uint256 totalTokens = tokenIds.length; uint256 totalTokensLeft; uint256 startTokenId; @@ -680,6 +691,12 @@ contract ERC721A is IERC721A { uint256 nextOwnershipPacked; uint256 quantity; + // If `approvalCheck` is true, check if the caller is approved for all token Ids. + // If approved for all, disable next approval checks. Otherwise keep them enabled + if (approvalCheck) + if (isApprovedForAll(from, _msgSenderERC721A())) + approvalCheck = false; + _beforeTokenBatchTransfers(from, to, tokenIds); // Underflow of the sender's balance is temporarily possible if the wrong set of token Ids is passed, @@ -710,7 +727,7 @@ contract ERC721A is IERC721A { ); // Clear approvals and emit transfer event for `startTokenId`. - _clearApprovalsAndEmitTransferEvent(from, toMasked, startTokenId, isApprovedForAll_); + _clearApprovalsAndEmitTransferEvent(from, toMasked, startTokenId, approvalCheck); // Derive quantity by looping over the next consecutive `totalTokensLeft`. for (quantity = 1; quantity < totalTokensLeft; ++quantity) { @@ -739,7 +756,7 @@ contract ERC721A is IERC721A { } nextOwnershipPacked = _packedOwnerships[nextTokenId]; - + // If the next slot's address is uninitialized. if (nextOwnershipPacked == 0) { // Revert if the next slot is out of bounds. Cannot be higher than `_currentIndex` since we're @@ -764,7 +781,7 @@ contract ERC721A is IERC721A { } // Clear approvals and emit transfer event for `nextTokenId`. - _clearApprovalsAndEmitTransferEvent(from, toMasked, nextTokenId, isApprovedForAll_); + _clearApprovalsAndEmitTransferEvent(from, toMasked, nextTokenId, approvalCheck); } // Skip the next `quantity` tokens. @@ -776,14 +793,38 @@ contract ERC721A is IERC721A { } /** - * @dev Equivalent to `_safeBatchTransferFrom(from, to, tokenIds, '')`. + * @dev Equivalent to `_safeBatchTransferFrom(from, to, tokenIds, false)`. */ function _safeBatchTransferFrom( address from, address to, uint256[] memory tokenIds ) internal virtual { - _safeBatchTransferFrom(from, to, tokenIds, ''); + _safeBatchTransferFrom(from, to, tokenIds, false); + } + + /** + * @dev Equivalent to `_safeBatchTransferFrom(from, to, tokenIds, '', approvalCheck)`. + */ + function _safeBatchTransferFrom( + address from, + address to, + uint256[] memory tokenIds, + bool approvalCheck + ) internal virtual { + _safeBatchTransferFrom(from, to, tokenIds, '', approvalCheck); + } + + /** + * @dev Equivalent to `_safeBatchTransferFrom(from, to, tokenIds, _data, false)`. + */ + function _safeBatchTransferFrom( + address from, + address to, + uint256[] memory tokenIds, + bytes memory _data + ) internal virtual { + _safeBatchTransferFrom(from, to, tokenIds, _data, false); } /** @@ -805,9 +846,10 @@ contract ERC721A is IERC721A { address from, address to, uint256[] memory tokenIds, - bytes memory _data + bytes memory _data, + bool approvalCheck ) internal virtual { - _batchTransferFrom(from, to, tokenIds); + _batchTransferFrom(from, to, tokenIds, approvalCheck); uint256 tokenId; uint256 totalTokens = tokenIds.length; @@ -947,15 +989,14 @@ contract ERC721A is IERC721A { address from, uint256 toMasked, uint256 tokenId, - bool isApprovedForAll_ + bool approvalCheck ) private { (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); - // The nested ifs save around 20+ gas over a compound boolean condition. - if (!isApprovedForAll_) + if (approvalCheck) { if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector); - + } // Clear approvals from the previous owner. assembly { if approvedAddress { From f52a3b8ce74be3e91770e406e5e8286e7c97407c Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Fri, 20 Jan 2023 02:50:35 +0100 Subject: [PATCH 11/74] fixed duplicate call --- contracts/ERC721A.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 92cb557b2..e8063cc07 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -732,7 +732,6 @@ contract ERC721A is IERC721A { // Derive quantity by looping over the next consecutive `totalTokensLeft`. for (quantity = 1; quantity < totalTokensLeft; ++quantity) { nextTokenId = startTokenId + quantity; - nextOwnershipPacked = _packedOwnerships[nextTokenId]; // If `nextTokenId` is not consecutive, update `nextOwnershipPacked` and break from the loop. if (tokenIds[i + quantity] != nextTokenId) { From 7a135c477a0ed3e66fd6ad229f6d4789a590b9ae Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Fri, 20 Jan 2023 02:52:35 +0100 Subject: [PATCH 12/74] comment --- contracts/ERC721A.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index e8063cc07..667b11a20 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -763,9 +763,8 @@ contract ERC721A is IERC721A { if (nextTokenId == _currentIndex) _revert(OwnerQueryForNonexistentToken.selector); // Otherwise we assume `from` owns `nextTokenId` and move on. } else { - // Revert if `nextTokenId` is not owned by `from`. + // Revert if `nextTokenId` is not owned by `from` or has been burned. if (address(uint160(nextOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); - // Revert if `nextTokenId` has been burned. if (nextOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); // Updates nextTokenId: From d454e3a90531a7d55eebad9aa9a7f684e9f8ecf1 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Fri, 20 Jan 2023 04:42:13 +0100 Subject: [PATCH 13/74] fixed next initialized --- contracts/ERC721A.sol | 56 +++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 667b11a20..bd3e881de 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -694,8 +694,7 @@ contract ERC721A is IERC721A { // If `approvalCheck` is true, check if the caller is approved for all token Ids. // If approved for all, disable next approval checks. Otherwise keep them enabled if (approvalCheck) - if (isApprovedForAll(from, _msgSenderERC721A())) - approvalCheck = false; + if (isApprovedForAll(from, _msgSenderERC721A())) approvalCheck = false; _beforeTokenBatchTransfers(from, to, tokenIds); @@ -716,39 +715,36 @@ contract ERC721A is IERC721A { prevOwnershipPacked = _packedOwnershipOf(startTokenId); if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); - // Updates startTokenId: - // - `address` to the next owner. - // - `startTimestamp` to the timestamp of transfering. - // - `burned` to `false`. - // - `nextInitialized` to `true`. - _packedOwnerships[startTokenId] = _packOwnershipData( - to, - _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked) - ); - // Clear approvals and emit transfer event for `startTokenId`. + // Call here to maintain correct event emission order in case consecutive tokens are transferred. _clearApprovalsAndEmitTransferEvent(from, toMasked, startTokenId, approvalCheck); // Derive quantity by looping over the next consecutive `totalTokensLeft`. for (quantity = 1; quantity < totalTokensLeft; ++quantity) { nextTokenId = startTokenId + quantity; + // TODO: Handle more efficiently + // Cache the last initialized ownership packed value from last loop. + uint256 lastInitOwnershipPacked; // If `nextTokenId` is not consecutive, update `nextOwnershipPacked` and break from the loop. if (tokenIds[i + quantity] != nextTokenId) { - // If `quantity` is 1, we can directly use `prevOwnershipPacked`. - // Otherwise we cannot assume _packedOwnershipOf(nextTokenId - 1) == prevOwnershipPacked. - uint256 lastOwnershipPacked = quantity == 1 - ? prevOwnershipPacked - : _packedOwnershipOf(nextTokenId - 1); - // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . - if (lastOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { + // If the next slot may not have been initialized (i.e. `nextInitialized == false`). + // If `quantity` is 1 we can use `prevOwnershipPacked`, otherwise `nextOwnershipPacked` from last loop. + if ( + (quantity == 1 ? prevOwnershipPacked : nextOwnershipPacked) & _BITMASK_NEXT_INITIALIZED == 0 + ) { // If the next slot's address is zero and not burned (i.e. packed value is zero). if (_packedOwnerships[nextTokenId] == 0) { // If the next slot is within bounds. if (nextTokenId != _currentIndex) { // Initialize the next slot to maintain correctness for `ownerOf(nextTokenId)`. - _packedOwnerships[nextTokenId] = lastOwnershipPacked; + if (lastInitOwnershipPacked != 0) { + _packedOwnerships[nextTokenId] = prevOwnershipPacked; + } else { + _packedOwnerships[nextTokenId] = lastInitOwnershipPacked; + } } + // Otherwise `nextTokenId - 1` is the last token ID, so there is no nextTokenId. } } break; @@ -756,7 +752,7 @@ contract ERC721A is IERC721A { nextOwnershipPacked = _packedOwnerships[nextTokenId]; - // If the next slot's address is uninitialized. + // If the next slot's address is zero and not burned (i.e. packed value is zero). if (nextOwnershipPacked == 0) { // Revert if the next slot is out of bounds. Cannot be higher than `_currentIndex` since we're // incrementing in steps of one @@ -767,14 +763,17 @@ contract ERC721A is IERC721A { if (address(uint160(nextOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); if (nextOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); + lastInitOwnershipPacked = nextOwnershipPacked; + // Updates nextTokenId: // - `address` to the next owner. // - `startTimestamp` to the timestamp of transfering. // - `burned` to `false`. - // - `nextInitialized` to `true`. + // - `nextInitialized` is left unchanged. _packedOwnerships[nextTokenId] = _packOwnershipData( to, - _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, nextOwnershipPacked) + (nextOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | + _nextExtraData(from, to, nextOwnershipPacked) ); } @@ -782,6 +781,17 @@ contract ERC721A is IERC721A { _clearApprovalsAndEmitTransferEvent(from, toMasked, nextTokenId, approvalCheck); } + // Updates startTokenId: + // - `address` to the next owner. + // - `startTimestamp` to the timestamp of transfering. + // - `burned` to `false`. + // - `nextInitialized` to `true` if `quantity` == 1, otherwise leave unchanged. + _packedOwnerships[startTokenId] = _packOwnershipData( + to, + (quantity == 1 ? _BITMASK_NEXT_INITIALIZED : prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | + _nextExtraData(from, to, prevOwnershipPacked) + ); + // Skip the next `quantity` tokens. i += quantity; } From 664882d590a6879227463d3f1bc61c85568ee9e2 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Fri, 20 Jan 2023 04:53:21 +0100 Subject: [PATCH 14/74] refactored lastInitPackedOwnership to use prevPackedOwnership --- contracts/ERC721A.sol | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index bd3e881de..57d48bab6 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -715,6 +715,17 @@ contract ERC721A is IERC721A { prevOwnershipPacked = _packedOwnershipOf(startTokenId); if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); + // Updates startTokenId: + // - `address` to the next owner. + // - `startTimestamp` to the timestamp of transfering. + // - `burned` to `false`. + // - `nextInitialized` is left unchanged. + _packedOwnerships[startTokenId] = _packOwnershipData( + to, + // TODO: Add (quantity == 1 ? _BITMASK_NEXT_INITIALIZED : prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | + (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | _nextExtraData(from, to, prevOwnershipPacked) + ); + // Clear approvals and emit transfer event for `startTokenId`. // Call here to maintain correct event emission order in case consecutive tokens are transferred. _clearApprovalsAndEmitTransferEvent(from, toMasked, startTokenId, approvalCheck); @@ -722,9 +733,6 @@ contract ERC721A is IERC721A { // Derive quantity by looping over the next consecutive `totalTokensLeft`. for (quantity = 1; quantity < totalTokensLeft; ++quantity) { nextTokenId = startTokenId + quantity; - // TODO: Handle more efficiently - // Cache the last initialized ownership packed value from last loop. - uint256 lastInitOwnershipPacked; // If `nextTokenId` is not consecutive, update `nextOwnershipPacked` and break from the loop. if (tokenIds[i + quantity] != nextTokenId) { @@ -738,11 +746,7 @@ contract ERC721A is IERC721A { // If the next slot is within bounds. if (nextTokenId != _currentIndex) { // Initialize the next slot to maintain correctness for `ownerOf(nextTokenId)`. - if (lastInitOwnershipPacked != 0) { - _packedOwnerships[nextTokenId] = prevOwnershipPacked; - } else { - _packedOwnerships[nextTokenId] = lastInitOwnershipPacked; - } + _packedOwnerships[nextTokenId] = prevOwnershipPacked; } // Otherwise `nextTokenId - 1` is the last token ID, so there is no nextTokenId. } @@ -763,7 +767,7 @@ contract ERC721A is IERC721A { if (address(uint160(nextOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); if (nextOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); - lastInitOwnershipPacked = nextOwnershipPacked; + prevOwnershipPacked = nextOwnershipPacked; // Updates nextTokenId: // - `address` to the next owner. @@ -781,17 +785,6 @@ contract ERC721A is IERC721A { _clearApprovalsAndEmitTransferEvent(from, toMasked, nextTokenId, approvalCheck); } - // Updates startTokenId: - // - `address` to the next owner. - // - `startTimestamp` to the timestamp of transfering. - // - `burned` to `false`. - // - `nextInitialized` to `true` if `quantity` == 1, otherwise leave unchanged. - _packedOwnerships[startTokenId] = _packOwnershipData( - to, - (quantity == 1 ? _BITMASK_NEXT_INITIALIZED : prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | - _nextExtraData(from, to, prevOwnershipPacked) - ); - // Skip the next `quantity` tokens. i += quantity; } From 64a88c459738a25fce7bb025f5f21f46fddf4dae Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Fri, 20 Jan 2023 16:36:28 +0100 Subject: [PATCH 15/74] comments --- contracts/ERC721A.sol | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 57d48bab6..03669b2c8 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -722,19 +722,20 @@ contract ERC721A is IERC721A { // - `nextInitialized` is left unchanged. _packedOwnerships[startTokenId] = _packOwnershipData( to, - // TODO: Add (quantity == 1 ? _BITMASK_NEXT_INITIALIZED : prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | + // TODO: Change to this? Costs +115 gas per [i] loop but ensures correctness of NEXT_INITIALIZED in startTokenId. + // If not consecutive set `nextInitialized` to true, otherwise keep it as is. + // (tokenIds[i + 1] != startTokenId + 1 ? _BITMASK_NEXT_INITIALIZED : prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | _nextExtraData(from, to, prevOwnershipPacked) ); // Clear approvals and emit transfer event for `startTokenId`. - // Call here to maintain correct event emission order in case consecutive tokens are transferred. _clearApprovalsAndEmitTransferEvent(from, toMasked, startTokenId, approvalCheck); // Derive quantity by looping over the next consecutive `totalTokensLeft`. for (quantity = 1; quantity < totalTokensLeft; ++quantity) { nextTokenId = startTokenId + quantity; - // If `nextTokenId` is not consecutive, update `nextOwnershipPacked` and break from the loop. + // If `nextTokenId` is not consecutive, update `nextTokenId` and break from the loop. if (tokenIds[i + quantity] != nextTokenId) { // If the next slot may not have been initialized (i.e. `nextInitialized == false`). // If `quantity` is 1 we can use `prevOwnershipPacked`, otherwise `nextOwnershipPacked` from last loop. @@ -748,7 +749,6 @@ contract ERC721A is IERC721A { // Initialize the next slot to maintain correctness for `ownerOf(nextTokenId)`. _packedOwnerships[nextTokenId] = prevOwnershipPacked; } - // Otherwise `nextTokenId - 1` is the last token ID, so there is no nextTokenId. } } break; @@ -767,6 +767,7 @@ contract ERC721A is IERC721A { if (address(uint160(nextOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); if (nextOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); + // Update `prevOwnershipPacked` with last initialized `nextOwnershipPacked`. prevOwnershipPacked = nextOwnershipPacked; // Updates nextTokenId: @@ -776,6 +777,9 @@ contract ERC721A is IERC721A { // - `nextInitialized` is left unchanged. _packedOwnerships[nextTokenId] = _packOwnershipData( to, + // TODO: Change to this? Costs +??? gas per [quantity] loop but ensures correctness of NEXT_INITIALIZED in nextTokenId. + // If nextTokenId + 1 is not consecutive set `nextInitialized` to true, otherwise keep it as is. + // (tokenIds[i + quantity + 1] != nextTokenId + 1 ? _BITMASK_NEXT_INITIALIZED : nextOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | (nextOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | _nextExtraData(from, to, nextOwnershipPacked) ); @@ -998,6 +1002,7 @@ contract ERC721A is IERC721A { if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector); } + // Clear approvals from the previous owner. assembly { if approvedAddress { From 96d7fb36d68db2d60e1adfdaea757f331335a516 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sat, 21 Jan 2023 17:11:17 +0100 Subject: [PATCH 16/74] ensured correctness of nextInitialized in slots of transferred token Ids --- contracts/ERC721A.sol | 54 ++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 03669b2c8..886a35d36 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -684,10 +684,10 @@ contract ERC721A is IERC721A { if (toMasked == 0) _revert(TransferToZeroAddress.selector); uint256 totalTokens = tokenIds.length; - uint256 totalTokensLeft; uint256 startTokenId; uint256 nextTokenId; uint256 prevOwnershipPacked; + uint256 lastOwnershipPacked; uint256 nextOwnershipPacked; uint256 quantity; @@ -709,45 +709,36 @@ contract ERC721A is IERC721A { for (uint256 i; i < totalTokens; ) { startTokenId = tokenIds[i]; - totalTokensLeft = totalTokens - i; // Check ownership of `startTokenId`. prevOwnershipPacked = _packedOwnershipOf(startTokenId); if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); - // Updates startTokenId: - // - `address` to the next owner. - // - `startTimestamp` to the timestamp of transfering. - // - `burned` to `false`. - // - `nextInitialized` is left unchanged. - _packedOwnerships[startTokenId] = _packOwnershipData( - to, - // TODO: Change to this? Costs +115 gas per [i] loop but ensures correctness of NEXT_INITIALIZED in startTokenId. - // If not consecutive set `nextInitialized` to true, otherwise keep it as is. - // (tokenIds[i + 1] != startTokenId + 1 ? _BITMASK_NEXT_INITIALIZED : prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | - (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | _nextExtraData(from, to, prevOwnershipPacked) - ); + lastOwnershipPacked = prevOwnershipPacked; // Clear approvals and emit transfer event for `startTokenId`. _clearApprovalsAndEmitTransferEvent(from, toMasked, startTokenId, approvalCheck); // Derive quantity by looping over the next consecutive `totalTokensLeft`. - for (quantity = 1; quantity < totalTokensLeft; ++quantity) { + for (quantity = 1; quantity < totalTokens - i; ++quantity) { nextTokenId = startTokenId + quantity; // If `nextTokenId` is not consecutive, update `nextTokenId` and break from the loop. if (tokenIds[i + quantity] != nextTokenId) { + // `lastOwnershipPacked` = last initialized slot before `nextTokenId` + // `nextOwnershipPacked` = slot of `nextTokenId - 1` + // If the next slot may not have been initialized (i.e. `nextInitialized == false`). - // If `quantity` is 1 we can use `prevOwnershipPacked`, otherwise `nextOwnershipPacked` from last loop. + // If `quantity` is 1 we use `lastOwnershipPacked`, otherwise `nextOwnershipPacked` from previous loop. if ( - (quantity == 1 ? prevOwnershipPacked : nextOwnershipPacked) & _BITMASK_NEXT_INITIALIZED == 0 + (quantity == 1 ? lastOwnershipPacked : nextOwnershipPacked) & _BITMASK_NEXT_INITIALIZED == 0 ) { // If the next slot's address is zero and not burned (i.e. packed value is zero). if (_packedOwnerships[nextTokenId] == 0) { // If the next slot is within bounds. if (nextTokenId != _currentIndex) { // Initialize the next slot to maintain correctness for `ownerOf(nextTokenId)`. - _packedOwnerships[nextTokenId] = prevOwnershipPacked; + _packedOwnerships[nextTokenId] = lastOwnershipPacked; } } } @@ -767,21 +758,21 @@ contract ERC721A is IERC721A { if (address(uint160(nextOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); if (nextOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); - // Update `prevOwnershipPacked` with last initialized `nextOwnershipPacked`. - prevOwnershipPacked = nextOwnershipPacked; + // Update `lastOwnershipPacked` with last initialized `nextOwnershipPacked`. + lastOwnershipPacked = nextOwnershipPacked; // Updates nextTokenId: // - `address` to the next owner. // - `startTimestamp` to the timestamp of transfering. // - `burned` to `false`. - // - `nextInitialized` is left unchanged. + // - `nextInitialized` to `true` when `nextTokenId + 1` is not consecutive, otherwise leave unchanged. _packedOwnerships[nextTokenId] = _packOwnershipData( to, - // TODO: Change to this? Costs +??? gas per [quantity] loop but ensures correctness of NEXT_INITIALIZED in nextTokenId. - // If nextTokenId + 1 is not consecutive set `nextInitialized` to true, otherwise keep it as is. - // (tokenIds[i + quantity + 1] != nextTokenId + 1 ? _BITMASK_NEXT_INITIALIZED : nextOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | - (nextOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | - _nextExtraData(from, to, nextOwnershipPacked) + ( + tokenIds[i + quantity + 1] != nextTokenId + 1 + ? _BITMASK_NEXT_INITIALIZED + : nextOwnershipPacked & _BITMASK_NEXT_INITIALIZED + ) | _nextExtraData(from, to, nextOwnershipPacked) ); } @@ -789,6 +780,17 @@ contract ERC721A is IERC721A { _clearApprovalsAndEmitTransferEvent(from, toMasked, nextTokenId, approvalCheck); } + // Updates startTokenId: + // - `address` to the next owner. + // - `startTimestamp` to the timestamp of transfering. + // - `burned` to `false`. + // - `nextInitialized` to `true` when `quantity == 1`, otherwise leave unchanged. + _packedOwnerships[startTokenId] = _packOwnershipData( + to, + (quantity == 1 ? _BITMASK_NEXT_INITIALIZED : prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | + _nextExtraData(from, to, prevOwnershipPacked) + ); + // Skip the next `quantity` tokens. i += quantity; } From 6a97e3b145c12f2b739f30016b29f489ed84ff55 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sat, 21 Jan 2023 18:04:14 +0100 Subject: [PATCH 17/74] renamed variables --- contracts/ERC721A.sol | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 886a35d36..45f2c56ba 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -686,8 +686,8 @@ contract ERC721A is IERC721A { uint256 totalTokens = tokenIds.length; uint256 startTokenId; uint256 nextTokenId; + uint256 startOwnershipPacked; uint256 prevOwnershipPacked; - uint256 lastOwnershipPacked; uint256 nextOwnershipPacked; uint256 quantity; @@ -711,10 +711,10 @@ contract ERC721A is IERC721A { startTokenId = tokenIds[i]; // Check ownership of `startTokenId`. - prevOwnershipPacked = _packedOwnershipOf(startTokenId); - if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); + startOwnershipPacked = _packedOwnershipOf(startTokenId); + if (address(uint160(startOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); - lastOwnershipPacked = prevOwnershipPacked; + prevOwnershipPacked = startOwnershipPacked; // Clear approvals and emit transfer event for `startTokenId`. _clearApprovalsAndEmitTransferEvent(from, toMasked, startTokenId, approvalCheck); @@ -725,20 +725,19 @@ contract ERC721A is IERC721A { // If `nextTokenId` is not consecutive, update `nextTokenId` and break from the loop. if (tokenIds[i + quantity] != nextTokenId) { - // `lastOwnershipPacked` = last initialized slot before `nextTokenId` + // `prevOwnershipPacked` = last initialized slot before `nextTokenId` // `nextOwnershipPacked` = slot of `nextTokenId - 1` // If the next slot may not have been initialized (i.e. `nextInitialized == false`). - // If `quantity` is 1 we use `lastOwnershipPacked`, otherwise `nextOwnershipPacked` from previous loop. if ( - (quantity == 1 ? lastOwnershipPacked : nextOwnershipPacked) & _BITMASK_NEXT_INITIALIZED == 0 + (quantity == 1 ? prevOwnershipPacked : nextOwnershipPacked) & _BITMASK_NEXT_INITIALIZED == 0 ) { // If the next slot's address is zero and not burned (i.e. packed value is zero). if (_packedOwnerships[nextTokenId] == 0) { // If the next slot is within bounds. if (nextTokenId != _currentIndex) { // Initialize the next slot to maintain correctness for `ownerOf(nextTokenId)`. - _packedOwnerships[nextTokenId] = lastOwnershipPacked; + _packedOwnerships[nextTokenId] = prevOwnershipPacked; } } } @@ -758,8 +757,8 @@ contract ERC721A is IERC721A { if (address(uint160(nextOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); if (nextOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); - // Update `lastOwnershipPacked` with last initialized `nextOwnershipPacked`. - lastOwnershipPacked = nextOwnershipPacked; + // Update `prevOwnershipPacked` with last initialized `nextOwnershipPacked`. + prevOwnershipPacked = nextOwnershipPacked; // Updates nextTokenId: // - `address` to the next owner. @@ -787,8 +786,8 @@ contract ERC721A is IERC721A { // - `nextInitialized` to `true` when `quantity == 1`, otherwise leave unchanged. _packedOwnerships[startTokenId] = _packOwnershipData( to, - (quantity == 1 ? _BITMASK_NEXT_INITIALIZED : prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | - _nextExtraData(from, to, prevOwnershipPacked) + (quantity == 1 ? _BITMASK_NEXT_INITIALIZED : startOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | + _nextExtraData(from, to, startOwnershipPacked) ); // Skip the next `quantity` tokens. From 3aa66fdd859880a49665ef8f62a3380fb2607679 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 25 Jan 2023 15:12:24 +0100 Subject: [PATCH 18/74] reverted to leave nextInitialized unchanged --- contracts/ERC721A.sol | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 45f2c56ba..81ee2614d 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -684,9 +684,9 @@ contract ERC721A is IERC721A { if (toMasked == 0) _revert(TransferToZeroAddress.selector); uint256 totalTokens = tokenIds.length; + uint256 totalTokensLeft; uint256 startTokenId; uint256 nextTokenId; - uint256 startOwnershipPacked; uint256 prevOwnershipPacked; uint256 nextOwnershipPacked; uint256 quantity; @@ -709,18 +709,27 @@ contract ERC721A is IERC721A { for (uint256 i; i < totalTokens; ) { startTokenId = tokenIds[i]; + totalTokensLeft = totalTokens - i; // Check ownership of `startTokenId`. - startOwnershipPacked = _packedOwnershipOf(startTokenId); - if (address(uint160(startOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); + prevOwnershipPacked = _packedOwnershipOf(startTokenId); + if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); - prevOwnershipPacked = startOwnershipPacked; + // Updates startTokenId: + // - `address` to the next owner. + // - `startTimestamp` to the timestamp of transfering. + // - `burned` to `false`. + // - `nextInitialized` is left unchanged. + _packedOwnerships[startTokenId] = _packOwnershipData( + to, + (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | _nextExtraData(from, to, prevOwnershipPacked) + ); // Clear approvals and emit transfer event for `startTokenId`. _clearApprovalsAndEmitTransferEvent(from, toMasked, startTokenId, approvalCheck); // Derive quantity by looping over the next consecutive `totalTokensLeft`. - for (quantity = 1; quantity < totalTokens - i; ++quantity) { + for (quantity = 1; quantity < totalTokensLeft; ++quantity) { nextTokenId = startTokenId + quantity; // If `nextTokenId` is not consecutive, update `nextTokenId` and break from the loop. @@ -764,14 +773,11 @@ contract ERC721A is IERC721A { // - `address` to the next owner. // - `startTimestamp` to the timestamp of transfering. // - `burned` to `false`. - // - `nextInitialized` to `true` when `nextTokenId + 1` is not consecutive, otherwise leave unchanged. + // - `nextInitialized` is left unchanged. _packedOwnerships[nextTokenId] = _packOwnershipData( to, - ( - tokenIds[i + quantity + 1] != nextTokenId + 1 - ? _BITMASK_NEXT_INITIALIZED - : nextOwnershipPacked & _BITMASK_NEXT_INITIALIZED - ) | _nextExtraData(from, to, nextOwnershipPacked) + (nextOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | + _nextExtraData(from, to, nextOwnershipPacked) ); } @@ -779,17 +785,6 @@ contract ERC721A is IERC721A { _clearApprovalsAndEmitTransferEvent(from, toMasked, nextTokenId, approvalCheck); } - // Updates startTokenId: - // - `address` to the next owner. - // - `startTimestamp` to the timestamp of transfering. - // - `burned` to `false`. - // - `nextInitialized` to `true` when `quantity == 1`, otherwise leave unchanged. - _packedOwnerships[startTokenId] = _packOwnershipData( - to, - (quantity == 1 ? _BITMASK_NEXT_INITIALIZED : startOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | - _nextExtraData(from, to, startOwnershipPacked) - ); - // Skip the next `quantity` tokens. i += quantity; } From a2de8922cd092c4080899998552e128800224f58 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 25 Jan 2023 15:17:43 +0100 Subject: [PATCH 19/74] comment --- contracts/ERC721A.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 81ee2614d..0884b30a0 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -658,6 +658,7 @@ contract ERC721A is IERC721A { /** * @dev Transfers `tokenIds` in batch from `from` to `to`. + * `tokenIds` should be provided sorted in ascending order to maximize efficiency. * * Requirements: * From 96bb77f920af6c3cf71498df3186b87607690749 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 25 Jan 2023 15:22:22 +0100 Subject: [PATCH 20/74] replace sort -> insertion sort --- contracts/ERC721A.sol | 150 +++++------------------------------------- 1 file changed, 16 insertions(+), 134 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 0884b30a0..71b9a0048 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1434,147 +1434,29 @@ contract ERC721A is IERC721A { } } - /** - * @dev Sorts the array in-place with intro-quicksort. - */ + /// @dev Sorts the array in-place with insertion sort. function _sort(uint256[] memory a) internal pure { /// @solidity memory-safe-assembly assembly { - let w := not(31) - let s := 0x20 let n := mload(a) // Length of `a`. mstore(a, 0) // For insertion sort's inner loop to terminate. - // Let the stack be the start of the free memory. - let stack := mload(0x40) - for {} iszero(lt(n, 2)) {} { - // Push `l` and `h` to the stack. - // The `shl` by 5 is equivalent to multiplying by `0x20`. - let l := add(a, s) - let h := add(a, shl(5, n)) - let j := l - // forgefmt: disable-next-item - for {} iszero(or(eq(j, h), gt(mload(j), mload(add(j, s))))) {} { - j := add(j, s) - } - // If the array is already sorted. - if eq(j, h) { break } - j := h - // forgefmt: disable-next-item - for {} iszero(gt(mload(j), mload(add(j, w)))) {} { + let h := add(a, shl(5, n)) // High slot. + let s := 0x20 + let w := not(31) + for { let i := add(a, s) } 1 {} { + i := add(i, s) + if gt(i, h) { break } + let k := mload(i) // Key. + let j := add(i, w) // The slot before the current slot. + let v := mload(j) // The value of `j`. + if iszero(gt(v, k)) { continue } + for {} 1 {} { + mstore(add(j, s), v) j := add(j, w) // `sub(j, 0x20)`. + v := mload(j) + if iszero(gt(v, k)) { break } } - // If the array is reversed sorted. - if eq(j, l) { - for {} 1 {} { - let t := mload(l) - mstore(l, mload(h)) - mstore(h, t) - h := add(h, w) // `sub(h, 0x20)`. - l := add(l, s) - if iszero(lt(l, h)) { break } - } - break - } - // Push `l` and `h` onto the stack. - mstore(stack, l) - mstore(add(stack, s), h) - stack := add(stack, 0x40) - break - } - for { let stackBottom := mload(0x40) } iszero(eq(stack, stackBottom)) {} { - // Pop `l` and `h` from the stack. - stack := sub(stack, 0x40) - let l := mload(stack) - let h := mload(add(stack, s)) - // Do insertion sort if `h - l <= 0x20 * 12`. - // Threshold is fine-tuned via trial and error. - if iszero(gt(sub(h, l), 0x180)) { - // Hardcode sort the first 2 elements. - let i := add(l, s) - if iszero(lt(mload(l), mload(i))) { - let t := mload(i) - mstore(i, mload(l)) - mstore(l, t) - } - for {} 1 {} { - i := add(i, s) - if gt(i, h) { break } - let k := mload(i) // Key. - let j := add(i, w) // The slot before the current slot. - let v := mload(j) // The value of `j`. - if iszero(gt(v, k)) { continue } - for {} 1 {} { - mstore(add(j, s), v) - j := add(j, w) - v := mload(j) - if iszero(gt(v, k)) { break } - } - mstore(add(j, s), k) - } - continue - } - // Pivot slot is the average of `l` and `h`, - // rounded down to nearest multiple of 0x20. - let p := shl(5, shr(6, add(l, h))) - // Median of 3 with sorting. - { - let e0 := mload(l) - let e2 := mload(h) - let e1 := mload(p) - if iszero(lt(e0, e1)) { - let t := e0 - e0 := e1 - e1 := t - } - if iszero(lt(e0, e2)) { - let t := e0 - e0 := e2 - e2 := t - } - if iszero(lt(e1, e2)) { - let t := e1 - e1 := e2 - e2 := t - } - mstore(p, e1) - mstore(h, e2) - mstore(l, e0) - } - // Hoare's partition. - { - // The value of the pivot slot. - let x := mload(p) - p := h - for { let i := l } 1 {} { - for {} 1 {} { - i := add(i, s) - if iszero(gt(x, mload(i))) { break } - } - let j := p - for {} 1 {} { - j := add(j, w) - if iszero(lt(x, mload(j))) { break } - } - p := j - if iszero(lt(i, p)) { break } - // Swap slots `i` and `p`. - let t := mload(i) - mstore(i, mload(p)) - mstore(p, t) - } - } - // If slice on right of pivot is non-empty, push onto stack. - { - mstore(stack, add(p, s)) - // Skip `mstore(add(stack, 0x20), h)`, as it is already on the stack. - stack := add(stack, shl(6, lt(add(p, s), h))) - } - // If slice on left of pivot is non-empty, push onto stack. - { - mstore(stack, l) - mstore(add(stack, s), p) - stack := add(stack, shl(6, gt(p, l))) - } + mstore(add(j, s), k) } mstore(a, n) // Restore the length of `a`. } From 1243741df5e9125e5024fb7ae1e985247fd863c9 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 25 Jan 2023 15:51:38 +0100 Subject: [PATCH 21/74] bump: prettier-plugin-solidity --- .prettierrc | 5 +- package-lock.json | 178 +++++++--------------------------------------- package.json | 2 +- 3 files changed, 29 insertions(+), 156 deletions(-) diff --git a/.prettierrc b/.prettierrc index 7ec948a64..2dbbc41f6 100644 --- a/.prettierrc +++ b/.prettierrc @@ -5,9 +5,8 @@ { "files": "*.sol", "options": { - "printWidth": 120, - "explicitTypes": "always" + "printWidth": 120 } } ] -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index ac85ca254..2d2a8599b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "hardhat": "^2.8.2", "hardhat-gas-reporter": "^1.0.7", "prettier": "^2.5.1", - "prettier-plugin-solidity": "^1.0.0-beta.19", + "prettier-plugin-solidity": "^1.1.1", "solidity-coverage": "^0.7.20" } }, @@ -1932,9 +1932,9 @@ } }, "node_modules/@solidity-parser/parser": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.0.tgz", - "integrity": "sha512-cX0JJRcmPtNUJpzD2K7FdA7qQsTOk1UZnFx2k7qAg9ZRvuaH5NBe5IEdBMXGlmf2+FmjhqbygJ26H8l2SV7aKQ==", + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.5.tgz", + "integrity": "sha512-6dKnHZn7fg/iQATVEzqyUOyEidbn05q7YA2mQ9hC0MMXhhV3/JrsxmFSYZAcr7j1yUP700LLhTruvJ3MiQmjJg==", "dev": true, "dependencies": { "antlr4ts": "^0.5.0-alpha.4" @@ -21826,59 +21826,20 @@ } }, "node_modules/prettier-plugin-solidity": { - "version": "1.0.0-beta.19", - "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0-beta.19.tgz", - "integrity": "sha512-xxRQ5ZiiZyUoMFLE9h7HnUDXI/daf1tnmL1msEdcKmyh7ZGQ4YklkYLC71bfBpYU2WruTb5/SFLUaEb3RApg5g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.1.tgz", + "integrity": "sha512-uD24KO26tAHF+zMN2nt1OUzfknzza5AgxjogQQrMLZc7j8xiQrDoNWNeOlfFC0YLTwo12CLD10b9niLyP6AqXg==", "dev": true, "dependencies": { - "@solidity-parser/parser": "^0.14.0", - "emoji-regex": "^10.0.0", - "escape-string-regexp": "^4.0.0", - "semver": "^7.3.5", - "solidity-comments-extractor": "^0.0.7", - "string-width": "^4.2.3" + "@solidity-parser/parser": "^0.14.5", + "semver": "^7.3.8", + "solidity-comments-extractor": "^0.0.7" }, "engines": { "node": ">=12" }, "peerDependencies": { - "prettier": "^2.3.0" - } - }, - "node_modules/prettier-plugin-solidity/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/prettier-plugin-solidity/node_modules/emoji-regex": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.0.0.tgz", - "integrity": "sha512-KmJa8l6uHi1HrBI34udwlzZY1jOEuID/ft4d8BSSEdRyap7PwBEt910453PJa5MuGvxkLqlt4Uvhu7tttFHViw==", - "dev": true - }, - "node_modules/prettier-plugin-solidity/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prettier-plugin-solidity/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" + "prettier": ">=2.3.0 || >=3.0.0-alpha.0" } }, "node_modules/prettier-plugin-solidity/node_modules/lru-cache": { @@ -21894,9 +21855,9 @@ } }, "node_modules/prettier-plugin-solidity/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -21908,38 +21869,6 @@ "node": ">=10" } }, - "node_modules/prettier-plugin-solidity/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/prettier-plugin-solidity/node_modules/string-width/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/prettier-plugin-solidity/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/prettier-plugin-solidity/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -28411,9 +28340,9 @@ } }, "@solidity-parser/parser": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.0.tgz", - "integrity": "sha512-cX0JJRcmPtNUJpzD2K7FdA7qQsTOk1UZnFx2k7qAg9ZRvuaH5NBe5IEdBMXGlmf2+FmjhqbygJ26H8l2SV7aKQ==", + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.5.tgz", + "integrity": "sha512-6dKnHZn7fg/iQATVEzqyUOyEidbn05q7YA2mQ9hC0MMXhhV3/JrsxmFSYZAcr7j1yUP700LLhTruvJ3MiQmjJg==", "dev": true, "requires": { "antlr4ts": "^0.5.0-alpha.4" @@ -44210,43 +44139,16 @@ "dev": true }, "prettier-plugin-solidity": { - "version": "1.0.0-beta.19", - "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0-beta.19.tgz", - "integrity": "sha512-xxRQ5ZiiZyUoMFLE9h7HnUDXI/daf1tnmL1msEdcKmyh7ZGQ4YklkYLC71bfBpYU2WruTb5/SFLUaEb3RApg5g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.1.tgz", + "integrity": "sha512-uD24KO26tAHF+zMN2nt1OUzfknzza5AgxjogQQrMLZc7j8xiQrDoNWNeOlfFC0YLTwo12CLD10b9niLyP6AqXg==", "dev": true, "requires": { - "@solidity-parser/parser": "^0.14.0", - "emoji-regex": "^10.0.0", - "escape-string-regexp": "^4.0.0", - "semver": "^7.3.5", - "solidity-comments-extractor": "^0.0.7", - "string-width": "^4.2.3" + "@solidity-parser/parser": "^0.14.5", + "semver": "^7.3.8", + "solidity-comments-extractor": "^0.0.7" }, "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "emoji-regex": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.0.0.tgz", - "integrity": "sha512-KmJa8l6uHi1HrBI34udwlzZY1jOEuID/ft4d8BSSEdRyap7PwBEt910453PJa5MuGvxkLqlt4Uvhu7tttFHViw==", - "dev": true - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -44257,42 +44159,14 @@ } }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, "requires": { "lru-cache": "^6.0.0" } }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "dependencies": { - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - } - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index f38d689e0..921e3db9f 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "hardhat": "^2.8.2", "hardhat-gas-reporter": "^1.0.7", "prettier": "^2.5.1", - "prettier-plugin-solidity": "^1.0.0-beta.19", + "prettier-plugin-solidity": "^1.1.1", "solidity-coverage": "^0.7.20" }, "repository": { From 67b710ba2de4a65b3017462a64ca70d68dbcbecb Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 25 Jan 2023 15:52:13 +0100 Subject: [PATCH 22/74] prettier --- contracts/ERC721A.sol | 106 ++++++------------ contracts/IERC721A.sol | 19 +--- contracts/extensions/ERC4907A.sol | 6 +- .../extensions/ERC721ABatchTransferable.sol | 6 +- contracts/extensions/ERC721AQueryable.sol | 20 +--- contracts/extensions/IERC4907A.sol | 6 +- .../extensions/IERC721ABatchTransferable.sol | 12 +- contracts/extensions/IERC721AQueryable.sol | 6 +- .../mocks/ERC721ABatchTransferableMock.sol | 6 +- contracts/mocks/ERC721AMock.sol | 6 +- 10 files changed, 51 insertions(+), 142 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 71b9a0048..189575a33 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -498,11 +498,9 @@ contract ERC721A is IERC721A { /** * @dev Returns the storage slot and value for the approved address of `tokenId`. */ - function _getApprovedSlotAndAddress(uint256 tokenId) - private - view - returns (uint256 approvedAddressSlot, address approvedAddress) - { + function _getApprovedSlotAndAddress( + uint256 tokenId + ) private view returns (uint256 approvedAddressSlot, address approvedAddress) { TokenApprovalRef storage tokenApproval = _tokenApprovals[tokenId]; // The following is equivalent to `approvedAddress = _tokenApprovals[tokenId].value`. assembly { @@ -528,11 +526,7 @@ contract ERC721A is IERC721A { * * Emits a {Transfer} event. */ - function transferFrom( - address from, - address to, - uint256 tokenId - ) public payable virtual override { + function transferFrom(address from, address to, uint256 tokenId) public payable virtual override { uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); // Mask `from` to the lower 160 bits, in case the upper bits somehow aren't clean. @@ -609,11 +603,7 @@ contract ERC721A is IERC721A { /** * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. */ - function safeTransferFrom( - address from, - address to, - uint256 tokenId - ) public payable virtual override { + function safeTransferFrom(address from, address to, uint256 tokenId) public payable virtual override { safeTransferFrom(from, to, tokenId, ''); } @@ -648,11 +638,7 @@ contract ERC721A is IERC721A { /** * @dev Equivalent to `_batchTransferFrom(from, to, tokenIds, false)`. */ - function _batchTransferFrom( - address from, - address to, - uint256[] memory tokenIds - ) internal virtual { + function _batchTransferFrom(address from, address to, uint256[] memory tokenIds) internal virtual { _batchTransferFrom(from, to, tokenIds, false); } @@ -797,11 +783,7 @@ contract ERC721A is IERC721A { /** * @dev Equivalent to `_safeBatchTransferFrom(from, to, tokenIds, false)`. */ - function _safeBatchTransferFrom( - address from, - address to, - uint256[] memory tokenIds - ) internal virtual { + function _safeBatchTransferFrom(address from, address to, uint256[] memory tokenIds) internal virtual { _safeBatchTransferFrom(from, to, tokenIds, false); } @@ -883,12 +865,7 @@ contract ERC721A is IERC721A { * - When `to` is zero, `tokenId` will be burned by `from`. * - `from` and `to` are never both zero. */ - function _beforeTokenTransfers( - address from, - address to, - uint256 startTokenId, - uint256 quantity - ) internal virtual {} + function _beforeTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} /** * @dev Hook that is called after a set of serially-ordered token IDs @@ -907,12 +884,7 @@ contract ERC721A is IERC721A { * - When `to` is zero, `tokenId` has been burned by `from`. * - `from` and `to` are never both zero. */ - function _afterTokenTransfers( - address from, - address to, - uint256 startTokenId, - uint256 quantity - ) internal virtual {} + function _afterTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} /** * @dev Hook that is called before a set of token IDs ordered in ascending order @@ -925,11 +897,7 @@ contract ERC721A is IERC721A { * - `from`'s `tokenIds` will be transferred to `to`. * - Neither `from` and `to` can be zero. */ - function _beforeTokenBatchTransfers( - address from, - address to, - uint256[] memory tokenIds - ) internal virtual {} + function _beforeTokenBatchTransfers(address from, address to, uint256[] memory tokenIds) internal virtual {} /** * @dev Hook that is called after a set of token IDs ordered in ascending order @@ -942,11 +910,7 @@ contract ERC721A is IERC721A { * - `from`'s `tokenIds` have been transferred to `to`. * - Neither `from` and `to` can be zero. */ - function _afterTokenBatchTransfers( - address from, - address to, - uint256[] memory tokenIds - ) internal virtual {} + function _afterTokenBatchTransfers(address from, address to, uint256[] memory tokenIds) internal virtual {} /** * @dev Private function to invoke {IERC721Receiver-onERC721Received} on a target contract. @@ -1159,11 +1123,7 @@ contract ERC721A is IERC721A { * * Emits a {Transfer} event for each mint. */ - function _safeMint( - address to, - uint256 quantity, - bytes memory _data - ) internal virtual { + function _safeMint(address to, uint256 quantity, bytes memory _data) internal virtual { _mint(to, quantity); unchecked { @@ -1212,11 +1172,7 @@ contract ERC721A is IERC721A { * * Emits an {Approval} event. */ - function _approve( - address to, - uint256 tokenId, - bool approvalCheck - ) internal virtual { + function _approve(address to, uint256 tokenId, bool approvalCheck) internal virtual { address owner = ownerOf(tokenId); if (approvalCheck && _msgSenderERC721A() != owner) @@ -1350,21 +1306,13 @@ contract ERC721A is IERC721A { * - When `to` is zero, `tokenId` will be burned by `from`. * - `from` and `to` are never both zero. */ - function _extraData( - address from, - address to, - uint24 previousExtraData - ) internal view virtual returns (uint24) {} + function _extraData(address from, address to, uint24 previousExtraData) internal view virtual returns (uint24) {} /** * @dev Returns the next extra data for the packed ownership data. * The returned result is shifted into position. */ - function _nextExtraData( - address from, - address to, - uint256 prevOwnershipPacked - ) private view returns (uint256) { + function _nextExtraData(address from, address to, uint256 prevOwnershipPacked) private view returns (uint256) { uint24 extraData = uint24(prevOwnershipPacked >> _BITPOS_EXTRA_DATA); return uint256(_extraData(from, to, extraData)) << _BITPOS_EXTRA_DATA; } @@ -1443,18 +1391,32 @@ contract ERC721A is IERC721A { let h := add(a, shl(5, n)) // High slot. let s := 0x20 let w := not(31) - for { let i := add(a, s) } 1 {} { + for { + let i := add(a, s) + } 1 { + + } { i := add(i, s) - if gt(i, h) { break } + if gt(i, h) { + break + } let k := mload(i) // Key. let j := add(i, w) // The slot before the current slot. let v := mload(j) // The value of `j`. - if iszero(gt(v, k)) { continue } - for {} 1 {} { + if iszero(gt(v, k)) { + continue + } + for { + + } 1 { + + } { mstore(add(j, s), v) j := add(j, w) // `sub(j, 0x20)`. v := mload(j) - if iszero(gt(v, k)) { break } + if iszero(gt(v, k)) { + break + } } mstore(add(j, s), k) } diff --git a/contracts/IERC721A.sol b/contracts/IERC721A.sol index 4bd87db45..abef5da6d 100644 --- a/contracts/IERC721A.sol +++ b/contracts/IERC721A.sol @@ -165,21 +165,12 @@ interface IERC721A { * * Emits a {Transfer} event. */ - function safeTransferFrom( - address from, - address to, - uint256 tokenId, - bytes calldata data - ) external payable; + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external payable; /** * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. */ - function safeTransferFrom( - address from, - address to, - uint256 tokenId - ) external payable; + function safeTransferFrom(address from, address to, uint256 tokenId) external payable; /** * @dev Transfers `tokenId` from `from` to `to`. @@ -197,11 +188,7 @@ interface IERC721A { * * Emits a {Transfer} event. */ - function transferFrom( - address from, - address to, - uint256 tokenId - ) external payable; + function transferFrom(address from, address to, uint256 tokenId) external payable; /** * @dev Gives permission to `to` to transfer `tokenId` token to another account. diff --git a/contracts/extensions/ERC4907A.sol b/contracts/extensions/ERC4907A.sol index fa0987c19..7e0821f69 100644 --- a/contracts/extensions/ERC4907A.sol +++ b/contracts/extensions/ERC4907A.sol @@ -33,11 +33,7 @@ abstract contract ERC4907A is ERC721A, IERC4907A { * * - The caller must own `tokenId` or be an approved operator. */ - function setUser( - uint256 tokenId, - address user, - uint64 expires - ) public virtual override { + function setUser(uint256 tokenId, address user, uint64 expires) public virtual override { // Require the caller to be either the token owner or an approved operator. address owner = ownerOf(tokenId); if (_msgSenderERC721A() != owner) diff --git a/contracts/extensions/ERC721ABatchTransferable.sol b/contracts/extensions/ERC721ABatchTransferable.sol index 8bb56a036..01e75dc6c 100644 --- a/contracts/extensions/ERC721ABatchTransferable.sol +++ b/contracts/extensions/ERC721ABatchTransferable.sol @@ -13,11 +13,7 @@ import './IERC721ABatchTransferable.sol'; * @dev ERC721A token optimized for batch transfers. */ abstract contract ERC721ABatchTransferable is ERC721A, IERC721ABatchTransferable { - function batchTransferFrom( - address from, - address to, - uint256[] memory tokenIds - ) public payable virtual override { + function batchTransferFrom(address from, address to, uint256[] memory tokenIds) public payable virtual override { _batchTransferFrom(from, to, tokenIds); } diff --git a/contracts/extensions/ERC721AQueryable.sol b/contracts/extensions/ERC721AQueryable.sol index 3b1813273..8bb90f00a 100644 --- a/contracts/extensions/ERC721AQueryable.sol +++ b/contracts/extensions/ERC721AQueryable.sol @@ -37,13 +37,9 @@ abstract contract ERC721AQueryable is ERC721A, IERC721AQueryable { * - `burned = false` * - `extraData = ` */ - function explicitOwnershipOf(uint256 tokenId) - public - view - virtual - override - returns (TokenOwnership memory ownership) - { + function explicitOwnershipOf( + uint256 tokenId + ) public view virtual override returns (TokenOwnership memory ownership) { if (tokenId >= _startTokenId()) { if (tokenId < _nextTokenId()) { ownership = _ownershipAt(tokenId); @@ -58,13 +54,9 @@ abstract contract ERC721AQueryable is ERC721A, IERC721AQueryable { * @dev Returns an array of `TokenOwnership` structs at `tokenIds` in order. * See {ERC721AQueryable-explicitOwnershipOf} */ - function explicitOwnershipsOf(uint256[] calldata tokenIds) - external - view - virtual - override - returns (TokenOwnership[] memory) - { + function explicitOwnershipsOf( + uint256[] calldata tokenIds + ) external view virtual override returns (TokenOwnership[] memory) { TokenOwnership[] memory ownerships; uint256 i = tokenIds.length; assembly { diff --git a/contracts/extensions/IERC4907A.sol b/contracts/extensions/IERC4907A.sol index 2e1b69017..abbd8e825 100644 --- a/contracts/extensions/IERC4907A.sol +++ b/contracts/extensions/IERC4907A.sol @@ -29,11 +29,7 @@ interface IERC4907A is IERC721A { * * - The caller must own `tokenId` or be an approved operator. */ - function setUser( - uint256 tokenId, - address user, - uint64 expires - ) external; + function setUser(uint256 tokenId, address user, uint64 expires) external; /** * @dev Returns the user address for `tokenId`. diff --git a/contracts/extensions/IERC721ABatchTransferable.sol b/contracts/extensions/IERC721ABatchTransferable.sol index 0b984e16b..084647b3a 100644 --- a/contracts/extensions/IERC721ABatchTransferable.sol +++ b/contracts/extensions/IERC721ABatchTransferable.sol @@ -23,20 +23,12 @@ interface IERC721ABatchTransferable is IERC721A { * * Emits a {Transfer} event for each transfer. */ - function batchTransferFrom( - address from, - address to, - uint256[] memory tokenIds - ) external payable; + function batchTransferFrom(address from, address to, uint256[] memory tokenIds) external payable; /** * @dev Equivalent to `safeBatchTransferFrom(from, to, tokenIds, '')`. */ - function safeBatchTransferFrom( - address from, - address to, - uint256[] memory tokenIds - ) external payable; + function safeBatchTransferFrom(address from, address to, uint256[] memory tokenIds) external payable; /** * @dev Safely transfers `tokenIds` in batch from `from` to `to`. See {ERC721A-_safeBatchTransferFrom}. diff --git a/contracts/extensions/IERC721AQueryable.sol b/contracts/extensions/IERC721AQueryable.sol index f0049462a..141bf1966 100644 --- a/contracts/extensions/IERC721AQueryable.sol +++ b/contracts/extensions/IERC721AQueryable.sol @@ -59,11 +59,7 @@ interface IERC721AQueryable is IERC721A { * * - `start < stop` */ - function tokensOfOwnerIn( - address owner, - uint256 start, - uint256 stop - ) external view returns (uint256[] memory); + function tokensOfOwnerIn(address owner, uint256 start, uint256 stop) external view returns (uint256[] memory); /** * @dev Returns an array of token IDs owned by `owner`. diff --git a/contracts/mocks/ERC721ABatchTransferableMock.sol b/contracts/mocks/ERC721ABatchTransferableMock.sol index 1aa081d46..4482fa4ea 100644 --- a/contracts/mocks/ERC721ABatchTransferableMock.sol +++ b/contracts/mocks/ERC721ABatchTransferableMock.sol @@ -37,11 +37,7 @@ contract ERC721ABatchTransferableMock is ERC721ABatchTransferable { _burn(tokenId, true); } - function batchTransferFromUnoptimized( - address from, - address to, - uint256[] memory tokenIds - ) public { + function batchTransferFromUnoptimized(address from, address to, uint256[] memory tokenIds) public { unchecked { uint256 tokenId; for (uint256 i; i < tokenIds.length; ++i) { diff --git a/contracts/mocks/ERC721AMock.sol b/contracts/mocks/ERC721AMock.sol index ca5e53523..12ed995b0 100644 --- a/contracts/mocks/ERC721AMock.sol +++ b/contracts/mocks/ERC721AMock.sol @@ -49,11 +49,7 @@ contract ERC721AMock is ERC721A { _safeMint(to, quantity); } - function safeMint( - address to, - uint256 quantity, - bytes memory _data - ) public { + function safeMint(address to, uint256 quantity, bytes memory _data) public { _safeMint(to, quantity, _data); } From 5987697479c104c7d178d9877e61377c52f0e4c3 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 25 Jan 2023 18:23:03 +0100 Subject: [PATCH 23/74] added prettier-ignore --- contracts/ERC721A.sol | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 189575a33..ba353fd4f 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1391,32 +1391,23 @@ contract ERC721A is IERC721A { let h := add(a, shl(5, n)) // High slot. let s := 0x20 let w := not(31) - for { - let i := add(a, s) - } 1 { - - } { + // prettier-ignore + for { let i := add(a, s) } 1 {} { i := add(i, s) - if gt(i, h) { - break - } + // prettier-ignore + if gt(i, h) { break } let k := mload(i) // Key. let j := add(i, w) // The slot before the current slot. let v := mload(j) // The value of `j`. - if iszero(gt(v, k)) { - continue - } - for { - - } 1 { - - } { + // prettier-ignore + if iszero(gt(v, k)) { continue } + // prettier-ignore + for {} 1 {} { mstore(add(j, s), v) j := add(j, w) // `sub(j, 0x20)`. v := mload(j) - if iszero(gt(v, k)) { - break - } + // prettier-ignore + if iszero(gt(v, k)) { break } } mstore(add(j, s), k) } From 7643010d2d0cb6638848ff0e975d26aaabd214a6 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Fri, 17 Feb 2023 04:56:11 +0100 Subject: [PATCH 24/74] fixed nextTokenId in last array element --- contracts/ERC721A.sol | 49 +++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index eaa6f087c..798b4e1d0 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -734,22 +734,7 @@ contract ERC721A is IERC721A { // If `nextTokenId` is not consecutive, update `nextTokenId` and break from the loop. if (tokenIds[i + quantity] != nextTokenId) { - // `prevOwnershipPacked` = last initialized slot before `nextTokenId` - // `nextOwnershipPacked` = slot of `nextTokenId - 1` - - // If the next slot may not have been initialized (i.e. `nextInitialized == false`). - if ( - (quantity == 1 ? prevOwnershipPacked : nextOwnershipPacked) & _BITMASK_NEXT_INITIALIZED == 0 - ) { - // If the next slot's address is zero and not burned (i.e. packed value is zero). - if (_packedOwnerships[nextTokenId] == 0) { - // If the next slot is within bounds. - if (nextTokenId != _currentIndex) { - // Initialize the next slot to maintain correctness for `ownerOf(nextTokenId)`. - _packedOwnerships[nextTokenId] = prevOwnershipPacked; - } - } - } + _updateNextTokenId(nextTokenId, quantity, prevOwnershipPacked, nextOwnershipPacked); break; } @@ -785,6 +770,11 @@ contract ERC721A is IERC721A { _clearApprovalsAndEmitTransferEvent(from, toMasked, nextTokenId, approvalCheck); } + // If the loop is over, update `nextTokenId + 1`. + if (totalTokensLeft == quantity) { + _updateNextTokenId(nextTokenId + 1, quantity, prevOwnershipPacked, nextOwnershipPacked); + } + // Skip the next `quantity` tokens. i += quantity; } @@ -955,6 +945,33 @@ contract ERC721A is IERC721A { } } + /** + * @dev Private function to handle updating ownership of `nextTokenId`. Used in `_batchTransferFrom`. + * + * `nextTokenId` - Token ID to update ownership of. + * `quantity` - Quantity of tokens transferred. + * `prevOwnershipPacked` - Last initialized slot before `nextTokenId` + * `nextOwnershipPacked` - Slot of `nextTokenId - 1` + */ + function _updateNextTokenId( + uint256 nextTokenId, + uint256 quantity, + uint256 prevOwnershipPacked, + uint256 nextOwnershipPacked + ) private { + // If the next slot may not have been initialized (i.e. `nextInitialized == false`). + if ((quantity == 1 ? prevOwnershipPacked : nextOwnershipPacked) & _BITMASK_NEXT_INITIALIZED == 0) { + // If the next slot's address is zero and not burned (i.e. packed value is zero). + if (_packedOwnerships[nextTokenId] == 0) { + // If the next slot is within bounds. + if (nextTokenId != _currentIndex) { + // Initialize the next slot to maintain correctness for `ownerOf(nextTokenId)`. + _packedOwnerships[nextTokenId] = prevOwnershipPacked; + } + } + } + } + /** * @dev Private function to handle clearing approvals and emitting transfer Event for a given `tokenId`. * Used in `_batchTransferFrom`. From c8ecb6aef22bc6a20ef23feb2a11b69d6e3d5482 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Fri, 17 Feb 2023 04:56:24 +0100 Subject: [PATCH 25/74] tests wip --- .../ERC721ABatchTransferable.test.js | 78 +++++++++++++++---- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/test/extensions/ERC721ABatchTransferable.test.js b/test/extensions/ERC721ABatchTransferable.test.js index 97b015007..e6f08e035 100644 --- a/test/extensions/ERC721ABatchTransferable.test.js +++ b/test/extensions/ERC721ABatchTransferable.test.js @@ -3,6 +3,8 @@ const { expect } = require('chai'); const { constants } = require('@openzeppelin/test-helpers'); const { ZERO_ADDRESS } = constants; +const RECEIVER_MAGIC_VALUE = '0x150b7a02'; + const createTestSuite = ({ contract, constructorArgs }) => function () { let offsetted; @@ -10,7 +12,10 @@ const createTestSuite = ({ contract, constructorArgs }) => context(`${contract}`, function () { beforeEach(async function () { this.erc721aBatchTransferable = await deployContract(contract, constructorArgs); - + this.receiver = await deployContract('ERC721ReceiverMock', [ + RECEIVER_MAGIC_VALUE, + this.erc721aBatchTransferable.address, + ]); this.startTokenId = this.erc721aBatchTransferable.startTokenId ? (await this.erc721aBatchTransferable.startTokenId()).toNumber() : 0; @@ -20,18 +25,14 @@ const createTestSuite = ({ contract, constructorArgs }) => }); beforeEach(async function () { - const [owner, addr1, addr2, addr3, addr4] = await ethers.getSigners(); + const [owner, addr1, addr2, addr3, addr4, addr5] = await ethers.getSigners(); this.owner = owner; this.addr1 = addr1; this.addr2 = addr2; this.addr3 = addr3; this.addr4 = addr4; - this.numTotalTokens = 20; - await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr2.address, 2); - await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr1.address, 1); - await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr2.address, 1); - await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr1.address, 2); - await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr2.address, 14); + this.addr5 = addr5; + this.numTotalTokens = 30; this.addr1.expected = { mintCount: 3, @@ -39,9 +40,21 @@ const createTestSuite = ({ contract, constructorArgs }) => }; this.addr2.expected = { - mintCount: 17, - tokens: offsetted(0, 17, 1, 6, 7, 13, 19, 10, 12, 11, 8, 14, 15, 16, 3, 18, 9), + mintCount: 20, + tokens: offsetted(0, 17, 1, 6, 7, 21, 13, 19, 10, 12, 11, 8, 20, 14, 15, 16, 3, 18, 22, 9), + }; + + this.addr3.expected = { + mintCount: 7, + tokens: offsetted(23, 24, 25, 26, 27, 28, 29), }; + + await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr2.address, 2); + await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr1.address, 1); + await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr2.address, 1); + await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr1.address, 2); + await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr2.address, 17); + await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr3.address, 7); }); context('test batch transfer functionality', function () { @@ -50,8 +63,12 @@ const createTestSuite = ({ contract, constructorArgs }) => const sender = this.addr2; this.tokenIds = this.addr2.expected.tokens; this.from = sender.address; - this.to = transferToContract ? this.addr3 : this.addr4; - await this.erc721aBatchTransferable.connect(sender).approve(this.to.address, this.tokenIds[0]); + this.to = transferToContract ? this.receiver : this.addr4; + this.approvedIds = [this.tokenIds[0], this.tokenIds[2], this.tokenIds[3]]; + + this.approvedIds.forEach(async (tokenId) => { + await this.erc721aBatchTransferable.connect(sender).approve(this.to.address, tokenId); + }); const ownershipBefore = await this.erc721aBatchTransferable.getOwnershipAt(this.tokenIds[0]); this.timestampBefore = parseInt(ownershipBefore.startTimestamp); @@ -61,7 +78,14 @@ const createTestSuite = ({ contract, constructorArgs }) => // prettier-ignore this.transferTx = await this.erc721aBatchTransferable - .connect(sender)[transferFn](this.from, this.to.address, this.tokenIds); + .connect(sender)[transferFn](this.from, this.to.address, this.tokenIds); + + // Transfer part of uninitialized tokens + this.tokensToTransferAlt = [25, 26, 27]; + // prettier-ignore + this.transferTxAlt = await this.erc721aBatchTransferable.connect(this.addr3)[transferFn]( + this.addr3.address, this.addr5.address, this.tokensToTransferAlt + ); const ownershipAfter = await this.erc721aBatchTransferable.getOwnershipAt(this.tokenIds[0]); this.timestampAfter = parseInt(ownershipAfter.startTimestamp); @@ -74,6 +98,28 @@ const createTestSuite = ({ contract, constructorArgs }) => } }); + it('transfers the ownership of uninitialized token IDs to the given address', async function () { + const allTokensInitiallyOwned = this.addr3.expected.tokens; + allTokensInitiallyOwned.splice(2, 3); + + for (let i = 0; i < this.tokensToTransferAlt.length; i++) { + const tokenId = this.tokensToTransferAlt[i]; + expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr5.address); + } + + for (let i = 0; i < allTokensInitiallyOwned.length; i++) { + const tokenId = allTokensInitiallyOwned[i]; + expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr3.address); + } + + expect(await this.erc721aBatchTransferable.balanceOf(this.addr5.address)).to.be.equal( + this.tokensToTransferAlt.length + ); + expect(await this.erc721aBatchTransferable.balanceOf(this.addr3.address)).to.be.equal( + allTokensInitiallyOwned.length + ); + }); + it('emits Transfers event', async function () { for (let i = 0; i < this.tokenIds.length; i++) { const tokenId = this.tokenIds[i]; @@ -83,8 +129,10 @@ const createTestSuite = ({ contract, constructorArgs }) => } }); - it('clears the approval for the token ID', async function () { - expect(await this.erc721aBatchTransferable.getApproved(this.tokenIds[0])).to.be.equal(ZERO_ADDRESS); + it('clears the approval for the token IDs', async function () { + this.approvedIds.forEach(async (tokenId) => { + expect(await this.erc721aBatchTransferable.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS); + }); }); it('adjusts owners balances', async function () { From 8e8987b066adb7912f8261c21b68046853383c61 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Fri, 17 Feb 2023 14:50:02 +0100 Subject: [PATCH 26/74] refactor --- contracts/ERC721A.sol | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 798b4e1d0..c3321c749 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -734,7 +734,11 @@ contract ERC721A is IERC721A { // If `nextTokenId` is not consecutive, update `nextTokenId` and break from the loop. if (tokenIds[i + quantity] != nextTokenId) { - _updateNextTokenId(nextTokenId, quantity, prevOwnershipPacked, nextOwnershipPacked); + _updateNextTokenId( + nextTokenId, + quantity == 1 ? prevOwnershipPacked : nextOwnershipPacked, + prevOwnershipPacked + ); break; } @@ -770,14 +774,16 @@ contract ERC721A is IERC721A { _clearApprovalsAndEmitTransferEvent(from, toMasked, nextTokenId, approvalCheck); } - // If the loop is over, update `nextTokenId + 1`. - if (totalTokensLeft == quantity) { - _updateNextTokenId(nextTokenId + 1, quantity, prevOwnershipPacked, nextOwnershipPacked); - } - // Skip the next `quantity` tokens. i += quantity; } + + // Update `nextTokenId + 1`, the tokenId subsequent to the last element in `tokenIds` + _updateNextTokenId( + nextTokenId + 1, + quantity == 1 ? prevOwnershipPacked : nextOwnershipPacked, + prevOwnershipPacked + ); } _afterTokenBatchTransfers(from, to, tokenIds); @@ -948,19 +954,13 @@ contract ERC721A is IERC721A { /** * @dev Private function to handle updating ownership of `nextTokenId`. Used in `_batchTransferFrom`. * - * `nextTokenId` - Token ID to update ownership of. - * `quantity` - Quantity of tokens transferred. + * `nextTokenId` - Token ID to initialize. + * `lastOwnershipPacked` - Slot of `nextTokenId - 1` * `prevOwnershipPacked` - Last initialized slot before `nextTokenId` - * `nextOwnershipPacked` - Slot of `nextTokenId - 1` */ - function _updateNextTokenId( - uint256 nextTokenId, - uint256 quantity, - uint256 prevOwnershipPacked, - uint256 nextOwnershipPacked - ) private { + function _updateNextTokenId(uint256 nextTokenId, uint256 lastOwnershipPacked, uint256 prevOwnershipPacked) private { // If the next slot may not have been initialized (i.e. `nextInitialized == false`). - if ((quantity == 1 ? prevOwnershipPacked : nextOwnershipPacked) & _BITMASK_NEXT_INITIALIZED == 0) { + if (lastOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { // If the next slot's address is zero and not burned (i.e. packed value is zero). if (_packedOwnerships[nextTokenId] == 0) { // If the next slot is within bounds. From b707c92f12dea906bd0273165f2a492352bd0bce Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Fri, 17 Feb 2023 16:26:12 +0100 Subject: [PATCH 27/74] updated BatchTransferable mock and extension --- contracts/extensions/ERC721ABatchTransferable.sol | 6 +++--- contracts/mocks/ERC721ABatchTransferableMock.sol | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/contracts/extensions/ERC721ABatchTransferable.sol b/contracts/extensions/ERC721ABatchTransferable.sol index 01e75dc6c..82175504d 100644 --- a/contracts/extensions/ERC721ABatchTransferable.sol +++ b/contracts/extensions/ERC721ABatchTransferable.sol @@ -14,7 +14,7 @@ import './IERC721ABatchTransferable.sol'; */ abstract contract ERC721ABatchTransferable is ERC721A, IERC721ABatchTransferable { function batchTransferFrom(address from, address to, uint256[] memory tokenIds) public payable virtual override { - _batchTransferFrom(from, to, tokenIds); + _batchTransferFrom(from, to, tokenIds, true); } function safeBatchTransferFrom( @@ -22,7 +22,7 @@ abstract contract ERC721ABatchTransferable is ERC721A, IERC721ABatchTransferable address to, uint256[] memory tokenIds ) public payable virtual override { - _safeBatchTransferFrom(from, to, tokenIds); + _safeBatchTransferFrom(from, to, tokenIds, true); } function safeBatchTransferFrom( @@ -31,6 +31,6 @@ abstract contract ERC721ABatchTransferable is ERC721A, IERC721ABatchTransferable uint256[] memory tokenIds, bytes memory _data ) public payable virtual override { - _safeBatchTransferFrom(from, to, tokenIds, _data); + _safeBatchTransferFrom(from, to, tokenIds, _data, true); } } diff --git a/contracts/mocks/ERC721ABatchTransferableMock.sol b/contracts/mocks/ERC721ABatchTransferableMock.sol index 4482fa4ea..b03405b41 100644 --- a/contracts/mocks/ERC721ABatchTransferableMock.sol +++ b/contracts/mocks/ERC721ABatchTransferableMock.sol @@ -37,6 +37,10 @@ contract ERC721ABatchTransferableMock is ERC721ABatchTransferable { _burn(tokenId, true); } + function initializeOwnershipAt(uint256 index) public { + _initializeOwnershipAt(index); + } + function batchTransferFromUnoptimized(address from, address to, uint256[] memory tokenIds) public { unchecked { uint256 tokenId; From e71a60c3e2f259328d2a23aa4e810e2ea0a3b95c Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Fri, 17 Feb 2023 16:26:24 +0100 Subject: [PATCH 28/74] updated tests --- .../ERC721ABatchTransferable.test.js | 121 +++++++++++++++++- 1 file changed, 116 insertions(+), 5 deletions(-) diff --git a/test/extensions/ERC721ABatchTransferable.test.js b/test/extensions/ERC721ABatchTransferable.test.js index e6f08e035..b64507ccc 100644 --- a/test/extensions/ERC721ABatchTransferable.test.js +++ b/test/extensions/ERC721ABatchTransferable.test.js @@ -76,6 +76,10 @@ const createTestSuite = ({ contract, constructorArgs }) => await mineBlockTimestamp(this.timestampToMine); this.timestampMined = await getBlockTimestamp(); + // Manually initialize some tokens of addr2 + await this.erc721aBatchTransferable.initializeOwnershipAt(3); + await this.erc721aBatchTransferable.initializeOwnershipAt(8); + // prettier-ignore this.transferTx = await this.erc721aBatchTransferable .connect(sender)[transferFn](this.from, this.to.address, this.tokenIds); @@ -96,11 +100,26 @@ const createTestSuite = ({ contract, constructorArgs }) => const tokenId = this.tokenIds[i]; expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.to.address); } + + // Initialized tokens were updated + expect((await this.erc721aBatchTransferable.getOwnershipAt(3))[0]).to.be.equal(this.to.address); + expect((await this.erc721aBatchTransferable.getOwnershipAt(8))[0]).to.be.equal(this.to.address); + + // Uninitialized tokens are left uninitialized + expect((await this.erc721aBatchTransferable.getOwnershipAt(7))[0]).to.be.equal( + transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.to.address + ); + + // Other tokens in between are left unchanged + for (let i = 0; i < this.addr1.expected.tokens.length; i++) { + const tokenId = this.addr1.expected.tokens[i]; + expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr1.address); + } }); it('transfers the ownership of uninitialized token IDs to the given address', async function () { const allTokensInitiallyOwned = this.addr3.expected.tokens; - allTokensInitiallyOwned.splice(2, 3); + allTokensInitiallyOwned.splice(2, this.tokensToTransferAlt.length); for (let i = 0; i < this.tokensToTransferAlt.length; i++) { const tokenId = this.tokensToTransferAlt[i]; @@ -112,11 +131,26 @@ const createTestSuite = ({ contract, constructorArgs }) => expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr3.address); } - expect(await this.erc721aBatchTransferable.balanceOf(this.addr5.address)).to.be.equal( - this.tokensToTransferAlt.length + // Ownership of tokens was updated + expect((await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[0]))[0]).to.be.equal( + this.addr5.address ); - expect(await this.erc721aBatchTransferable.balanceOf(this.addr3.address)).to.be.equal( - allTokensInitiallyOwned.length + expect((await this.erc721aBatchTransferable.getOwnershipAt(allTokensInitiallyOwned[2]))[0]).to.be.equal( + this.addr3.address + ); + + // Uninitialized tokens are left uninitialized + expect( + (await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[0] - 1))[0] + ).to.be.equal(ZERO_ADDRESS); + expect((await this.erc721aBatchTransferable.getOwnershipAt(allTokensInitiallyOwned[3]))[0]).to.be.equal( + ZERO_ADDRESS + ); + expect((await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[1]))[0]).to.be.equal( + transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.addr5.address + ); + expect((await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[2]))[0]).to.be.equal( + transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.addr5.address ); }); @@ -140,6 +174,12 @@ const createTestSuite = ({ contract, constructorArgs }) => expect(await this.erc721aBatchTransferable.balanceOf(this.to.address)).to.be.equal( this.addr2.expected.mintCount ); + expect(await this.erc721aBatchTransferable.balanceOf(this.addr3.address)).to.be.equal( + this.addr3.expected.tokens.length - this.tokensToTransferAlt.length + ); + expect(await this.erc721aBatchTransferable.balanceOf(this.addr5.address)).to.be.equal( + this.tokensToTransferAlt.length + ); }); it('startTimestamp updated correctly', async function () { @@ -150,6 +190,45 @@ const createTestSuite = ({ contract, constructorArgs }) => }); }; + const testUnsuccessfulBatchTransfer = function (transferFn) { + beforeEach(function () { + this.tokenIds = this.addr2.expected.tokens.slice(0, 2); + this.sender = this.addr1; + }); + + it('rejects unapproved transfer', async function () { + // prettier-ignore + await expect( + this.erc721aBatchTransferable + .connect(this.sender)[transferFn]( + this.addr2.address, this.sender.address, this.tokenIds + ) + ).to.be.revertedWith('TransferCallerNotOwnerNorApproved'); + }); + + it('rejects transfer from incorrect owner', async function () { + await this.erc721aBatchTransferable.connect(this.addr2).setApprovalForAll(this.sender.address, true); + // prettier-ignore + await expect( + this.erc721aBatchTransferable + .connect(this.sender)[transferFn]( + this.addr3.address, this.sender.address, this.tokenIds + ) + ).to.be.revertedWith('TransferFromIncorrectOwner'); + }); + + it('rejects transfer to zero address', async function () { + await this.erc721aBatchTransferable.connect(this.addr2).setApprovalForAll(this.sender.address, true); + // prettier-ignore + await expect( + this.erc721aBatchTransferable + .connect(this.sender)[transferFn]( + this.addr2.address, ZERO_ADDRESS, this.tokenIds + ) + ).to.be.revertedWith('TransferToZeroAddress'); + }); + }; + context('successful transfers', function () { context('batchTransferFrom', function () { describe('to contract', function () { @@ -181,6 +260,38 @@ const createTestSuite = ({ contract, constructorArgs }) => }); }); }); + + context('unsuccessful transfers', function () { + context('batchTransferFrom', function () { + describe('to contract', function () { + testUnsuccessfulBatchTransfer('batchTransferFrom'); + }); + + describe('to EOA', function () { + testUnsuccessfulBatchTransfer('batchTransferFrom', false); + }); + }); + context('safeBatchTransferFrom', function () { + describe('to contract', function () { + testUnsuccessfulBatchTransfer('safeBatchTransferFrom(address,address,uint256[])'); + }); + + describe('to EOA', function () { + testUnsuccessfulBatchTransfer('safeBatchTransferFrom(address,address,uint256[])', false); + }); + }); + + // TEMPORARY: to use as comparison for gas usage + context('batchTransferFromUnoptimized', function () { + describe('to contract', function () { + testUnsuccessfulBatchTransfer('batchTransferFromUnoptimized'); + }); + + describe('to EOA', function () { + testUnsuccessfulBatchTransfer('batchTransferFromUnoptimized', false); + }); + }); + }); }); }); }; From ae13aacbaf971a7e266d52196ee604351a875da1 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Fri, 17 Feb 2023 16:45:49 +0100 Subject: [PATCH 29/74] add approval tests --- .../ERC721ABatchTransferable.test.js | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/test/extensions/ERC721ABatchTransferable.test.js b/test/extensions/ERC721ABatchTransferable.test.js index b64507ccc..1e018c689 100644 --- a/test/extensions/ERC721ABatchTransferable.test.js +++ b/test/extensions/ERC721ABatchTransferable.test.js @@ -229,6 +229,68 @@ const createTestSuite = ({ contract, constructorArgs }) => }); }; + const testApproveBatchTransfer = function (transferFn) { + beforeEach(function () { + this.tokenIds = this.addr1.expected.tokens.slice(0, 2); + }); + + it('approval allows batch transfers', async function () { + // prettier-ignore + await expect( + this.erc721aBatchTransferable + .connect(this.addr3)[transferFn]( + this.addr1.address, this.addr3.address, this.tokenIds + ) + ).to.be.revertedWith('TransferCallerNotOwnerNorApproved'); + + for (let i = 0; i < this.tokenIds.length; i++) { + const tokenId = this.tokenIds[i]; + await this.erc721aBatchTransferable.connect(this.addr1).approve(this.addr3.address, tokenId); + } + + // prettier-ignore + await this.erc721aBatchTransferable + .connect(this.addr3)[transferFn]( + this.addr1.address, this.addr3.address, this.tokenIds + ); + // prettier-ignore + await expect( + this.erc721aBatchTransferable + .connect(this.addr1)[transferFn]( + this.addr3.address, this.addr1.address, this.tokenIds + ) + ).to.be.revertedWith('TransferCallerNotOwnerNorApproved'); + }); + + it('self-approval is cleared on batch transfers', async function () { + for (let i = 0; i < this.tokenIds.length; i++) { + const tokenId = this.tokenIds[i]; + await this.erc721aBatchTransferable.connect(this.addr1).approve(this.addr1.address, tokenId); + expect(await this.erc721aBatchTransferable.getApproved(tokenId)).to.equal(this.addr1.address); + } + + // prettier-ignore + await this.erc721aBatchTransferable + .connect(this.addr1)[transferFn]( + this.addr1.address, this.addr2.address, this.tokenIds + ); + for (let i = 0; i < this.tokenIds.length; i++) { + const tokenId = this.tokenIds[i]; + expect(await this.erc721aBatchTransferable.getApproved(tokenId)).to.not.equal(this.addr1.address); + } + }); + + it('approval for all allows batch transfers', async function () { + await this.erc721aBatchTransferable.connect(this.addr1).setApprovalForAll(this.addr3.address, true); + + // prettier-ignore + await this.erc721aBatchTransferable + .connect(this.addr3)[transferFn]( + this.addr1.address, this.addr3.address, this.tokenIds + ); + }); + }; + context('successful transfers', function () { context('batchTransferFrom', function () { describe('to contract', function () { @@ -292,6 +354,38 @@ const createTestSuite = ({ contract, constructorArgs }) => }); }); }); + + context('approvals', function () { + context('batchTransferFrom', function () { + describe('to contract', function () { + testApproveBatchTransfer('batchTransferFrom'); + }); + + describe('to EOA', function () { + testApproveBatchTransfer('batchTransferFrom', false); + }); + }); + context('safeBatchTransferFrom', function () { + describe('to contract', function () { + testApproveBatchTransfer('safeBatchTransferFrom(address,address,uint256[])'); + }); + + describe('to EOA', function () { + testApproveBatchTransfer('safeBatchTransferFrom(address,address,uint256[])', false); + }); + }); + + // TEMPORARY: to use as comparison for gas usage + context('batchTransferFromUnoptimized', function () { + describe('to contract', function () { + testApproveBatchTransfer('batchTransferFromUnoptimized'); + }); + + describe('to EOA', function () { + testApproveBatchTransfer('batchTransferFromUnoptimized', false); + }); + }); + }); }); }); }; From 62d0af4de7a7304e99a99140cabfd753a651d9d6 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Fri, 17 Feb 2023 17:07:03 +0100 Subject: [PATCH 30/74] lint --- contracts/extensions/ERC721AQueryable.sol | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/contracts/extensions/ERC721AQueryable.sol b/contracts/extensions/ERC721AQueryable.sol index 5bb9ee340..47a3ec123 100644 --- a/contracts/extensions/ERC721AQueryable.sol +++ b/contracts/extensions/ERC721AQueryable.sol @@ -37,13 +37,9 @@ abstract contract ERC721AQueryable is ERC721A, IERC721AQueryable { * - `burned = false` * - `extraData = ` */ - function explicitOwnershipOf(uint256 tokenId) - public - view - virtual - override - returns (TokenOwnership memory ownership) - { + function explicitOwnershipOf( + uint256 tokenId + ) public view virtual override returns (TokenOwnership memory ownership) { unchecked { if (tokenId >= _startTokenId()) { if (tokenId < _nextTokenId()) { @@ -134,11 +130,7 @@ abstract contract ERC721AQueryable is ERC721A, IERC721AQueryable { * Note that this function is optimized for smaller bytecode size over runtime gas, * since it is meant to be called off-chain. */ - function _tokensOfOwnerIn( - address owner, - uint256 start, - uint256 stop - ) private view returns (uint256[] memory) { + function _tokensOfOwnerIn(address owner, uint256 start, uint256 stop) private view returns (uint256[] memory) { unchecked { if (start >= stop) _revert(InvalidQueryRange.selector); // Set `start = max(start, _startTokenId())`. From ce6bed4bb60afc582bfa9cecd197c05eeae316c9 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sat, 18 Feb 2023 14:07:36 +0100 Subject: [PATCH 31/74] lint fix --- contracts/ERC721A.sol | 88 ++++++++-- contracts/IERC721A.sol | 19 ++- contracts/extensions/ERC4907A.sol | 6 +- .../extensions/ERC721ABatchTransferable.sol | 6 +- contracts/extensions/ERC721AQueryable.sol | 26 ++- contracts/extensions/IERC4907A.sol | 6 +- .../extensions/IERC721ABatchTransferable.sol | 12 +- contracts/extensions/IERC721AQueryable.sol | 6 +- .../mocks/ERC721ABatchTransferableMock.sol | 6 +- contracts/mocks/ERC721AMock.sol | 6 +- package-lock.json | 154 ++++++++++++++++-- package.json | 2 +- 12 files changed, 288 insertions(+), 49 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index c3321c749..daa251362 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -511,9 +511,11 @@ contract ERC721A is IERC721A { /** * @dev Returns the storage slot and value for the approved address of `tokenId`. */ - function _getApprovedSlotAndAddress( - uint256 tokenId - ) private view returns (uint256 approvedAddressSlot, address approvedAddress) { + function _getApprovedSlotAndAddress(uint256 tokenId) + private + view + returns (uint256 approvedAddressSlot, address approvedAddress) + { TokenApprovalRef storage tokenApproval = _tokenApprovals[tokenId]; // The following is equivalent to `approvedAddress = _tokenApprovals[tokenId].value`. assembly { @@ -539,7 +541,11 @@ contract ERC721A is IERC721A { * * Emits a {Transfer} event. */ - function transferFrom(address from, address to, uint256 tokenId) public payable virtual override { + function transferFrom( + address from, + address to, + uint256 tokenId + ) public payable virtual override { uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); // Mask `from` to the lower 160 bits, in case the upper bits somehow aren't clean. @@ -616,7 +622,11 @@ contract ERC721A is IERC721A { /** * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. */ - function safeTransferFrom(address from, address to, uint256 tokenId) public payable virtual override { + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public payable virtual override { safeTransferFrom(from, to, tokenId, ''); } @@ -651,7 +661,11 @@ contract ERC721A is IERC721A { /** * @dev Equivalent to `_batchTransferFrom(from, to, tokenIds, false)`. */ - function _batchTransferFrom(address from, address to, uint256[] memory tokenIds) internal virtual { + function _batchTransferFrom( + address from, + address to, + uint256[] memory tokenIds + ) internal virtual { _batchTransferFrom(from, to, tokenIds, false); } @@ -792,7 +806,11 @@ contract ERC721A is IERC721A { /** * @dev Equivalent to `_safeBatchTransferFrom(from, to, tokenIds, false)`. */ - function _safeBatchTransferFrom(address from, address to, uint256[] memory tokenIds) internal virtual { + function _safeBatchTransferFrom( + address from, + address to, + uint256[] memory tokenIds + ) internal virtual { _safeBatchTransferFrom(from, to, tokenIds, false); } @@ -874,7 +892,12 @@ contract ERC721A is IERC721A { * - When `to` is zero, `tokenId` will be burned by `from`. * - `from` and `to` are never both zero. */ - function _beforeTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} + function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual {} /** * @dev Hook that is called after a set of serially-ordered token IDs @@ -893,7 +916,12 @@ contract ERC721A is IERC721A { * - When `to` is zero, `tokenId` has been burned by `from`. * - `from` and `to` are never both zero. */ - function _afterTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} + function _afterTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual {} /** * @dev Hook that is called before a set of token IDs ordered in ascending order @@ -906,7 +934,11 @@ contract ERC721A is IERC721A { * - `from`'s `tokenIds` will be transferred to `to`. * - Neither `from` and `to` can be zero. */ - function _beforeTokenBatchTransfers(address from, address to, uint256[] memory tokenIds) internal virtual {} + function _beforeTokenBatchTransfers( + address from, + address to, + uint256[] memory tokenIds + ) internal virtual {} /** * @dev Hook that is called after a set of token IDs ordered in ascending order @@ -919,7 +951,11 @@ contract ERC721A is IERC721A { * - `from`'s `tokenIds` have been transferred to `to`. * - Neither `from` and `to` can be zero. */ - function _afterTokenBatchTransfers(address from, address to, uint256[] memory tokenIds) internal virtual {} + function _afterTokenBatchTransfers( + address from, + address to, + uint256[] memory tokenIds + ) internal virtual {} /** * @dev Private function to invoke {IERC721Receiver-onERC721Received} on a target contract. @@ -958,7 +994,11 @@ contract ERC721A is IERC721A { * `lastOwnershipPacked` - Slot of `nextTokenId - 1` * `prevOwnershipPacked` - Last initialized slot before `nextTokenId` */ - function _updateNextTokenId(uint256 nextTokenId, uint256 lastOwnershipPacked, uint256 prevOwnershipPacked) private { + function _updateNextTokenId( + uint256 nextTokenId, + uint256 lastOwnershipPacked, + uint256 prevOwnershipPacked + ) private { // If the next slot may not have been initialized (i.e. `nextInitialized == false`). if (lastOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { // If the next slot's address is zero and not burned (i.e. packed value is zero). @@ -1153,7 +1193,11 @@ contract ERC721A is IERC721A { * * Emits a {Transfer} event for each mint. */ - function _safeMint(address to, uint256 quantity, bytes memory _data) internal virtual { + function _safeMint( + address to, + uint256 quantity, + bytes memory _data + ) internal virtual { _mint(to, quantity); unchecked { @@ -1202,7 +1246,11 @@ contract ERC721A is IERC721A { * * Emits an {Approval} event. */ - function _approve(address to, uint256 tokenId, bool approvalCheck) internal virtual { + function _approve( + address to, + uint256 tokenId, + bool approvalCheck + ) internal virtual { address owner = ownerOf(tokenId); if (approvalCheck && _msgSenderERC721A() != owner) @@ -1336,13 +1384,21 @@ contract ERC721A is IERC721A { * - When `to` is zero, `tokenId` will be burned by `from`. * - `from` and `to` are never both zero. */ - function _extraData(address from, address to, uint24 previousExtraData) internal view virtual returns (uint24) {} + function _extraData( + address from, + address to, + uint24 previousExtraData + ) internal view virtual returns (uint24) {} /** * @dev Returns the next extra data for the packed ownership data. * The returned result is shifted into position. */ - function _nextExtraData(address from, address to, uint256 prevOwnershipPacked) private view returns (uint256) { + function _nextExtraData( + address from, + address to, + uint256 prevOwnershipPacked + ) private view returns (uint256) { uint24 extraData = uint24(prevOwnershipPacked >> _BITPOS_EXTRA_DATA); return uint256(_extraData(from, to, extraData)) << _BITPOS_EXTRA_DATA; } diff --git a/contracts/IERC721A.sol b/contracts/IERC721A.sol index abef5da6d..4bd87db45 100644 --- a/contracts/IERC721A.sol +++ b/contracts/IERC721A.sol @@ -165,12 +165,21 @@ interface IERC721A { * * Emits a {Transfer} event. */ - function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external payable; + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes calldata data + ) external payable; /** * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. */ - function safeTransferFrom(address from, address to, uint256 tokenId) external payable; + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) external payable; /** * @dev Transfers `tokenId` from `from` to `to`. @@ -188,7 +197,11 @@ interface IERC721A { * * Emits a {Transfer} event. */ - function transferFrom(address from, address to, uint256 tokenId) external payable; + function transferFrom( + address from, + address to, + uint256 tokenId + ) external payable; /** * @dev Gives permission to `to` to transfer `tokenId` token to another account. diff --git a/contracts/extensions/ERC4907A.sol b/contracts/extensions/ERC4907A.sol index 7e0821f69..fa0987c19 100644 --- a/contracts/extensions/ERC4907A.sol +++ b/contracts/extensions/ERC4907A.sol @@ -33,7 +33,11 @@ abstract contract ERC4907A is ERC721A, IERC4907A { * * - The caller must own `tokenId` or be an approved operator. */ - function setUser(uint256 tokenId, address user, uint64 expires) public virtual override { + function setUser( + uint256 tokenId, + address user, + uint64 expires + ) public virtual override { // Require the caller to be either the token owner or an approved operator. address owner = ownerOf(tokenId); if (_msgSenderERC721A() != owner) diff --git a/contracts/extensions/ERC721ABatchTransferable.sol b/contracts/extensions/ERC721ABatchTransferable.sol index 82175504d..d4caa4a0a 100644 --- a/contracts/extensions/ERC721ABatchTransferable.sol +++ b/contracts/extensions/ERC721ABatchTransferable.sol @@ -13,7 +13,11 @@ import './IERC721ABatchTransferable.sol'; * @dev ERC721A token optimized for batch transfers. */ abstract contract ERC721ABatchTransferable is ERC721A, IERC721ABatchTransferable { - function batchTransferFrom(address from, address to, uint256[] memory tokenIds) public payable virtual override { + function batchTransferFrom( + address from, + address to, + uint256[] memory tokenIds + ) public payable virtual override { _batchTransferFrom(from, to, tokenIds, true); } diff --git a/contracts/extensions/ERC721AQueryable.sol b/contracts/extensions/ERC721AQueryable.sol index 47a3ec123..081815bd3 100644 --- a/contracts/extensions/ERC721AQueryable.sol +++ b/contracts/extensions/ERC721AQueryable.sol @@ -37,9 +37,13 @@ abstract contract ERC721AQueryable is ERC721A, IERC721AQueryable { * - `burned = false` * - `extraData = ` */ - function explicitOwnershipOf( - uint256 tokenId - ) public view virtual override returns (TokenOwnership memory ownership) { + function explicitOwnershipOf(uint256 tokenId) + public + view + virtual + override + returns (TokenOwnership memory ownership) + { unchecked { if (tokenId >= _startTokenId()) { if (tokenId < _nextTokenId()) { @@ -56,9 +60,13 @@ abstract contract ERC721AQueryable is ERC721A, IERC721AQueryable { * @dev Returns an array of `TokenOwnership` structs at `tokenIds` in order. * See {ERC721AQueryable-explicitOwnershipOf} */ - function explicitOwnershipsOf( - uint256[] calldata tokenIds - ) external view virtual override returns (TokenOwnership[] memory) { + function explicitOwnershipsOf(uint256[] calldata tokenIds) + external + view + virtual + override + returns (TokenOwnership[] memory) + { TokenOwnership[] memory ownerships; uint256 i = tokenIds.length; assembly { @@ -130,7 +138,11 @@ abstract contract ERC721AQueryable is ERC721A, IERC721AQueryable { * Note that this function is optimized for smaller bytecode size over runtime gas, * since it is meant to be called off-chain. */ - function _tokensOfOwnerIn(address owner, uint256 start, uint256 stop) private view returns (uint256[] memory) { + function _tokensOfOwnerIn( + address owner, + uint256 start, + uint256 stop + ) private view returns (uint256[] memory) { unchecked { if (start >= stop) _revert(InvalidQueryRange.selector); // Set `start = max(start, _startTokenId())`. diff --git a/contracts/extensions/IERC4907A.sol b/contracts/extensions/IERC4907A.sol index abbd8e825..2e1b69017 100644 --- a/contracts/extensions/IERC4907A.sol +++ b/contracts/extensions/IERC4907A.sol @@ -29,7 +29,11 @@ interface IERC4907A is IERC721A { * * - The caller must own `tokenId` or be an approved operator. */ - function setUser(uint256 tokenId, address user, uint64 expires) external; + function setUser( + uint256 tokenId, + address user, + uint64 expires + ) external; /** * @dev Returns the user address for `tokenId`. diff --git a/contracts/extensions/IERC721ABatchTransferable.sol b/contracts/extensions/IERC721ABatchTransferable.sol index 084647b3a..0b984e16b 100644 --- a/contracts/extensions/IERC721ABatchTransferable.sol +++ b/contracts/extensions/IERC721ABatchTransferable.sol @@ -23,12 +23,20 @@ interface IERC721ABatchTransferable is IERC721A { * * Emits a {Transfer} event for each transfer. */ - function batchTransferFrom(address from, address to, uint256[] memory tokenIds) external payable; + function batchTransferFrom( + address from, + address to, + uint256[] memory tokenIds + ) external payable; /** * @dev Equivalent to `safeBatchTransferFrom(from, to, tokenIds, '')`. */ - function safeBatchTransferFrom(address from, address to, uint256[] memory tokenIds) external payable; + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory tokenIds + ) external payable; /** * @dev Safely transfers `tokenIds` in batch from `from` to `to`. See {ERC721A-_safeBatchTransferFrom}. diff --git a/contracts/extensions/IERC721AQueryable.sol b/contracts/extensions/IERC721AQueryable.sol index 141bf1966..f0049462a 100644 --- a/contracts/extensions/IERC721AQueryable.sol +++ b/contracts/extensions/IERC721AQueryable.sol @@ -59,7 +59,11 @@ interface IERC721AQueryable is IERC721A { * * - `start < stop` */ - function tokensOfOwnerIn(address owner, uint256 start, uint256 stop) external view returns (uint256[] memory); + function tokensOfOwnerIn( + address owner, + uint256 start, + uint256 stop + ) external view returns (uint256[] memory); /** * @dev Returns an array of token IDs owned by `owner`. diff --git a/contracts/mocks/ERC721ABatchTransferableMock.sol b/contracts/mocks/ERC721ABatchTransferableMock.sol index b03405b41..0442e25fc 100644 --- a/contracts/mocks/ERC721ABatchTransferableMock.sol +++ b/contracts/mocks/ERC721ABatchTransferableMock.sol @@ -41,7 +41,11 @@ contract ERC721ABatchTransferableMock is ERC721ABatchTransferable { _initializeOwnershipAt(index); } - function batchTransferFromUnoptimized(address from, address to, uint256[] memory tokenIds) public { + function batchTransferFromUnoptimized( + address from, + address to, + uint256[] memory tokenIds + ) public { unchecked { uint256 tokenId; for (uint256 i; i < tokenIds.length; ++i) { diff --git a/contracts/mocks/ERC721AMock.sol b/contracts/mocks/ERC721AMock.sol index 221ec34fa..89beacc6a 100644 --- a/contracts/mocks/ERC721AMock.sol +++ b/contracts/mocks/ERC721AMock.sol @@ -50,7 +50,11 @@ contract ERC721AMock is ERC721A, DirectBurnBitSetterHelper { _safeMint(to, quantity); } - function safeMint(address to, uint256 quantity, bytes memory _data) public { + function safeMint( + address to, + uint256 quantity, + bytes memory _data + ) public { _safeMint(to, quantity, _data); } diff --git a/package-lock.json b/package-lock.json index 2d2a8599b..6e73dad9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "hardhat": "^2.8.2", "hardhat-gas-reporter": "^1.0.7", "prettier": "^2.5.1", - "prettier-plugin-solidity": "^1.1.1", + "prettier-plugin-solidity": "1.0.0-beta.19", "solidity-coverage": "^0.7.20" } }, @@ -21826,20 +21826,59 @@ } }, "node_modules/prettier-plugin-solidity": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.1.tgz", - "integrity": "sha512-uD24KO26tAHF+zMN2nt1OUzfknzza5AgxjogQQrMLZc7j8xiQrDoNWNeOlfFC0YLTwo12CLD10b9niLyP6AqXg==", + "version": "1.0.0-beta.19", + "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0-beta.19.tgz", + "integrity": "sha512-xxRQ5ZiiZyUoMFLE9h7HnUDXI/daf1tnmL1msEdcKmyh7ZGQ4YklkYLC71bfBpYU2WruTb5/SFLUaEb3RApg5g==", "dev": true, "dependencies": { - "@solidity-parser/parser": "^0.14.5", - "semver": "^7.3.8", - "solidity-comments-extractor": "^0.0.7" + "@solidity-parser/parser": "^0.14.0", + "emoji-regex": "^10.0.0", + "escape-string-regexp": "^4.0.0", + "semver": "^7.3.5", + "solidity-comments-extractor": "^0.0.7", + "string-width": "^4.2.3" }, "engines": { "node": ">=12" }, "peerDependencies": { - "prettier": ">=2.3.0 || >=3.0.0-alpha.0" + "prettier": "^2.3.0" + } + }, + "node_modules/prettier-plugin-solidity/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/prettier-plugin-solidity/node_modules/emoji-regex": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.2.1.tgz", + "integrity": "sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==", + "dev": true + }, + "node_modules/prettier-plugin-solidity/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prettier-plugin-solidity/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" } }, "node_modules/prettier-plugin-solidity/node_modules/lru-cache": { @@ -21869,6 +21908,38 @@ "node": ">=10" } }, + "node_modules/prettier-plugin-solidity/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prettier-plugin-solidity/node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/prettier-plugin-solidity/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/prettier-plugin-solidity/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -44139,16 +44210,43 @@ "dev": true }, "prettier-plugin-solidity": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.1.tgz", - "integrity": "sha512-uD24KO26tAHF+zMN2nt1OUzfknzza5AgxjogQQrMLZc7j8xiQrDoNWNeOlfFC0YLTwo12CLD10b9niLyP6AqXg==", + "version": "1.0.0-beta.19", + "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0-beta.19.tgz", + "integrity": "sha512-xxRQ5ZiiZyUoMFLE9h7HnUDXI/daf1tnmL1msEdcKmyh7ZGQ4YklkYLC71bfBpYU2WruTb5/SFLUaEb3RApg5g==", "dev": true, "requires": { - "@solidity-parser/parser": "^0.14.5", - "semver": "^7.3.8", - "solidity-comments-extractor": "^0.0.7" + "@solidity-parser/parser": "^0.14.0", + "emoji-regex": "^10.0.0", + "escape-string-regexp": "^4.0.0", + "semver": "^7.3.5", + "solidity-comments-extractor": "^0.0.7", + "string-width": "^4.2.3" }, "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "emoji-regex": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.2.1.tgz", + "integrity": "sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -44167,6 +44265,34 @@ "lru-cache": "^6.0.0" } }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + } + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 921e3db9f..f38d689e0 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "hardhat": "^2.8.2", "hardhat-gas-reporter": "^1.0.7", "prettier": "^2.5.1", - "prettier-plugin-solidity": "^1.1.1", + "prettier-plugin-solidity": "^1.0.0-beta.19", "solidity-coverage": "^0.7.20" }, "repository": { From 951afa388dcf87c61f6d49170371cda476b43303 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sat, 18 Feb 2023 14:09:44 +0100 Subject: [PATCH 32/74] restore original .prettierrc --- .prettierrc | 3 ++- package-lock.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.prettierrc b/.prettierrc index 2dbbc41f6..3dff7f42f 100644 --- a/.prettierrc +++ b/.prettierrc @@ -5,7 +5,8 @@ { "files": "*.sol", "options": { - "printWidth": 120 + "printWidth": 120, + "explicitTypes": "always" } } ] diff --git a/package-lock.json b/package-lock.json index 6e73dad9c..11bff36c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "hardhat": "^2.8.2", "hardhat-gas-reporter": "^1.0.7", "prettier": "^2.5.1", - "prettier-plugin-solidity": "1.0.0-beta.19", + "prettier-plugin-solidity": "^1.0.0-beta.19", "solidity-coverage": "^0.7.20" } }, From 0b9ce74ca8839685112cf07ae9a39dc9dbe3ea66 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sat, 18 Feb 2023 14:12:36 +0100 Subject: [PATCH 33/74] fix --- package-lock.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 11bff36c6..ac85ca254 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1932,9 +1932,9 @@ } }, "node_modules/@solidity-parser/parser": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.5.tgz", - "integrity": "sha512-6dKnHZn7fg/iQATVEzqyUOyEidbn05q7YA2mQ9hC0MMXhhV3/JrsxmFSYZAcr7j1yUP700LLhTruvJ3MiQmjJg==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.0.tgz", + "integrity": "sha512-cX0JJRcmPtNUJpzD2K7FdA7qQsTOk1UZnFx2k7qAg9ZRvuaH5NBe5IEdBMXGlmf2+FmjhqbygJ26H8l2SV7aKQ==", "dev": true, "dependencies": { "antlr4ts": "^0.5.0-alpha.4" @@ -21855,9 +21855,9 @@ } }, "node_modules/prettier-plugin-solidity/node_modules/emoji-regex": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.2.1.tgz", - "integrity": "sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.0.0.tgz", + "integrity": "sha512-KmJa8l6uHi1HrBI34udwlzZY1jOEuID/ft4d8BSSEdRyap7PwBEt910453PJa5MuGvxkLqlt4Uvhu7tttFHViw==", "dev": true }, "node_modules/prettier-plugin-solidity/node_modules/escape-string-regexp": { @@ -21894,9 +21894,9 @@ } }, "node_modules/prettier-plugin-solidity/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -28411,9 +28411,9 @@ } }, "@solidity-parser/parser": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.5.tgz", - "integrity": "sha512-6dKnHZn7fg/iQATVEzqyUOyEidbn05q7YA2mQ9hC0MMXhhV3/JrsxmFSYZAcr7j1yUP700LLhTruvJ3MiQmjJg==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.0.tgz", + "integrity": "sha512-cX0JJRcmPtNUJpzD2K7FdA7qQsTOk1UZnFx2k7qAg9ZRvuaH5NBe5IEdBMXGlmf2+FmjhqbygJ26H8l2SV7aKQ==", "dev": true, "requires": { "antlr4ts": "^0.5.0-alpha.4" @@ -44230,9 +44230,9 @@ "dev": true }, "emoji-regex": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.2.1.tgz", - "integrity": "sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.0.0.tgz", + "integrity": "sha512-KmJa8l6uHi1HrBI34udwlzZY1jOEuID/ft4d8BSSEdRyap7PwBEt910453PJa5MuGvxkLqlt4Uvhu7tttFHViw==", "dev": true }, "escape-string-regexp": { @@ -44257,9 +44257,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" From 76c5bf64d07fd13230c06bdae2c33683eef9be2a Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 20 Feb 2023 01:27:09 +0100 Subject: [PATCH 34/74] comments and refactor --- contracts/ERC721A.sol | 92 +++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index daa251362..933dd918a 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -689,7 +689,7 @@ contract ERC721A is IERC721A { uint256[] memory tokenIds, bool approvalCheck ) internal virtual { - // Sort `tokenIds` to allow batching consecutive ids into single operations. + // Sort `tokenIds` to allow batching consecutive ids. _sort(tokenIds); // Mask `from` and `to` to the lower 160 bits, in case the upper bits somehow aren't clean. @@ -925,7 +925,7 @@ contract ERC721A is IERC721A { /** * @dev Hook that is called before a set of token IDs ordered in ascending order - * are about to be transferred. Only called on batch transfers. + * are about to be transferred. Only called on batch transfers and burns. * * `tokenIds` - the array of tokenIds to be transferred, ordered in ascending order. * @@ -942,7 +942,7 @@ contract ERC721A is IERC721A { /** * @dev Hook that is called after a set of token IDs ordered in ascending order - * have been transferred. Only called on batch transfers. + * have been transferred. Only called on batch transfers and burns. * * `tokenIds` - the array of tokenIds transferred, ordered in ascending order. * @@ -1012,49 +1012,6 @@ contract ERC721A is IERC721A { } } - /** - * @dev Private function to handle clearing approvals and emitting transfer Event for a given `tokenId`. - * Used in `_batchTransferFrom`. - * - * `from` - Previous owner of the given token ID. - * `toMasked` - Target address that will receive the token. - * `tokenId` - Token ID to be transferred. - * `isApprovedForAll_` - Whether the caller is approved for all token IDs. - */ - function _clearApprovalsAndEmitTransferEvent( - address from, - uint256 toMasked, - uint256 tokenId, - bool approvalCheck - ) private { - (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); - - if (approvalCheck) { - if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) - _revert(TransferCallerNotOwnerNorApproved.selector); - } - - // Clear approvals from the previous owner. - assembly { - if approvedAddress { - // This is equivalent to `delete _tokenApprovals[tokenId]`. - sstore(approvedAddressSlot, 0) - } - } - - assembly { - // Emit the `Transfer` event. - log4( - 0, // Start of data (0, since no data). - 0, // End of data (0, since no data). - _TRANSFER_EVENT_SIGNATURE, // Signature. - from, // `from`. - toMasked, // `to`. - tokenId // `tokenId`. - ) - } - } - // ============================================================= // MINT OPERATIONS // ============================================================= @@ -1262,6 +1219,49 @@ contract ERC721A is IERC721A { emit Approval(owner, to, tokenId); } + /** + * @dev Private function to handle clearing approvals and emitting transfer Event for a given `tokenId`. + * Used in `_batchTransferFrom`. + * + * `from` - Previous owner of the given token ID. + * `toMasked` - Target address that will receive the token. + * `tokenId` - Token ID to be transferred. + * `isApprovedForAll_` - Whether the caller is approved for all token IDs. + */ + function _clearApprovalsAndEmitTransferEvent( + address from, + uint256 toMasked, + uint256 tokenId, + bool approvalCheck + ) private { + (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); + + if (approvalCheck) { + if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) + _revert(TransferCallerNotOwnerNorApproved.selector); + } + + // Clear approvals from the previous owner. + assembly { + if approvedAddress { + // This is equivalent to `delete _tokenApprovals[tokenId]`. + sstore(approvedAddressSlot, 0) + } + } + + assembly { + // Emit the `Transfer` event. + log4( + 0, // Start of data (0, since no data). + 0, // End of data (0, since no data). + _TRANSFER_EVENT_SIGNATURE, // Signature. + from, // `from`. + toMasked, // `to`. + tokenId // `tokenId`. + ) + } + } + // ============================================================= // BURN OPERATIONS // ============================================================= From 9e91b646b24afa027a6824fbc2ea9faa19283288 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 20 Feb 2023 01:27:22 +0100 Subject: [PATCH 35/74] added _batchBurn --- contracts/ERC721A.sol | 132 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 933dd918a..2c719e5fd 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1351,6 +1351,138 @@ contract ERC721A is IERC721A { } } + /** + * @dev Equivalent to `_batchBurn(tokenIds, false)`. + */ + function _batchBurn(uint256[] memory tokenIds) internal virtual { + _batchBurn(tokenIds, false); + } + + /** + * @dev Destroys `tokenIds` in batch. + * The approval is cleared when the tokens are burned. + * `tokenIds` should be provided sorted in ascending order to maximize efficiency. + * + * Requirements: + * + * - `tokenIds` must exist. + * + * Emits a {Transfer} event for each burn. + */ + function _batchBurn(uint256[] memory tokenIds, bool approvalCheck) internal virtual { + // Sort `tokenIds` to allow batching consecutive ids. + _sort(tokenIds); + + uint256 totalTokens = tokenIds.length; + uint256 totalTokensLeft; + uint256 startTokenId = tokenIds[0]; + uint256 nextTokenId; + uint256 prevOwnershipPacked = _packedOwnershipOf(startTokenId); + uint256 nextOwnershipPacked; + uint256 quantity; + + address from = address(uint160(prevOwnershipPacked)); + + // If `approvalCheck` is true, check if the caller is approved for all token Ids. + // If approved for all, disable next approval checks. Otherwise keep them enabled + if (approvalCheck) + if (isApprovedForAll(from, _msgSenderERC721A())) approvalCheck = false; + + _beforeTokenBatchTransfers(from, address(0), tokenIds); + + // Underflow of the sender's balance is temporarily possible if the wrong set of token Ids is passed, + // but reverts afterwards when ownership is checked. + // The recipient's balance can't realistically overflow. + // Counter overflow is incredibly unrealistic as `startTokenId` would have to be 2**256. + unchecked { + // Updates: + // - `balance -= totalTokens`. + // - `numberBurned += totalTokens`. + // + // We can directly decrement the balance, and increment the number burned. + // This is equivalent to `packed -= totalTokens; packed += totalTokens << _BITPOS_NUMBER_BURNED;`. + _packedAddressData[from] += (totalTokens << _BITPOS_NUMBER_BURNED) - totalTokens; + + for (uint256 i; i < totalTokens; ) { + // Except during first loop, where this logic has already been executed. + if (i != 0) { + startTokenId = tokenIds[i]; + totalTokensLeft = totalTokens - i; + + // Check ownership of `startTokenId`. + prevOwnershipPacked = _packedOwnershipOf(startTokenId); + } + + // Updates startTokenId: + // - `address` to the last owner. + // - `startTimestamp` to the timestamp of burning. + // - `burned` to `true`. + // - `nextInitialized` is left unchanged. + _packedOwnerships[startTokenId] = _packOwnershipData( + from, + (_BITMASK_BURNED | (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED)) | + _nextExtraData(from, address(0), prevOwnershipPacked) + ); + + // Clear approvals and emit transfer event for `startTokenId`. + _clearApprovalsAndEmitTransferEvent(from, 0, startTokenId, approvalCheck); + + // Derive quantity by looping over the next consecutive `totalTokensLeft`. + for (quantity = 1; quantity < totalTokensLeft; ++quantity) { + nextTokenId = startTokenId + quantity; + + // If `nextTokenId` is not consecutive, update `nextTokenId` and break from the loop. + if (tokenIds[i + quantity] != nextTokenId) { + _updateNextTokenId( + nextTokenId, + quantity == 1 ? prevOwnershipPacked : nextOwnershipPacked, + prevOwnershipPacked + ); + break; + } + + nextOwnershipPacked = _packedOwnerships[nextTokenId]; + + // If the next slot's address is zero and not burned (i.e. packed value is zero). + if (nextOwnershipPacked == 0) { + // Revert if the next slot is out of bounds. Cannot be higher than `_currentIndex` since we're + // incrementing in steps of one + if (nextTokenId == _currentIndex) _revert(OwnerQueryForNonexistentToken.selector); + // Otherwise we assume `from` owns `nextTokenId` and move on. + } else { + // Revert if `nextTokenId` is not owned by `from` or has been burned. + if (address(uint160(nextOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); + if (nextOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); + + // Update `prevOwnershipPacked` with last initialized `nextOwnershipPacked`. + prevOwnershipPacked = nextOwnershipPacked; + + // Clear the slot to leverage consecutive burns optimization. + delete _packedOwnerships[nextTokenId]; + } + + // Clear approvals and emit transfer event for `nextTokenId`. + _clearApprovalsAndEmitTransferEvent(from, 0, nextTokenId, approvalCheck); + } + + // Skip the next `quantity` tokens. + i += quantity; + } + + // Update `nextTokenId + 1`, the tokenId subsequent to the last element in `tokenIds` + _updateNextTokenId( + nextTokenId + 1, + quantity == 1 ? prevOwnershipPacked : nextOwnershipPacked, + prevOwnershipPacked + ); + + // Overflow not possible, as _burnCounter cannot be exceed _currentIndex times. + _burnCounter += totalTokens; + } + + _afterTokenBatchTransfers(from, address(0), tokenIds); + } + // ============================================================= // EXTRA DATA OPERATIONS // ============================================================= From ca0fa7cdaa88a9fdc152d59428762c7d42cc7554 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 20 Feb 2023 01:37:46 +0100 Subject: [PATCH 36/74] added ERC721ABatchBurnable extension, interfaces and mock --- contracts/extensions/ERC721ABatchBurnable.sol | 19 +++++++ .../extensions/IERC721ABatchBurnable.sol | 14 ++++++ .../interfaces/IERC721ABatchBurnable.sol | 7 +++ contracts/mocks/ERC721ABatchBurnableMock.sol | 49 +++++++++++++++++++ 4 files changed, 89 insertions(+) create mode 100644 contracts/extensions/ERC721ABatchBurnable.sol create mode 100644 contracts/extensions/IERC721ABatchBurnable.sol create mode 100644 contracts/interfaces/IERC721ABatchBurnable.sol create mode 100644 contracts/mocks/ERC721ABatchBurnableMock.sol diff --git a/contracts/extensions/ERC721ABatchBurnable.sol b/contracts/extensions/ERC721ABatchBurnable.sol new file mode 100644 index 000000000..93299e99e --- /dev/null +++ b/contracts/extensions/ERC721ABatchBurnable.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v4.2.3 +// Creator: Chiru Labs + +pragma solidity ^0.8.4; + +import './ERC721ABurnable.sol'; +import './IERC721ABatchBurnable.sol'; + +/** + * @title ERC721ABatchBurnable. + * + * @dev ERC721A token optimized for batch burns. + */ +abstract contract ERC721ABatchBurnable is ERC721ABurnable, IERC721ABatchBurnable { + function batchBurn(uint256[] memory tokenIds) public virtual override { + _batchBurn(tokenIds, true); + } +} diff --git a/contracts/extensions/IERC721ABatchBurnable.sol b/contracts/extensions/IERC721ABatchBurnable.sol new file mode 100644 index 000000000..2c7840ad4 --- /dev/null +++ b/contracts/extensions/IERC721ABatchBurnable.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v4.2.3 +// Creator: Chiru Labs + +pragma solidity ^0.8.4; + +import './IERC721ABurnable.sol'; + +/** + * @dev Interface of ERC721ABatchBurnable. + */ +interface IERC721ABatchBurnable is IERC721ABurnable { + function batchBurn(uint256[] memory tokenIds) external; +} diff --git a/contracts/interfaces/IERC721ABatchBurnable.sol b/contracts/interfaces/IERC721ABatchBurnable.sol new file mode 100644 index 000000000..3074153eb --- /dev/null +++ b/contracts/interfaces/IERC721ABatchBurnable.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v4.2.3 +// Creator: Chiru Labs + +pragma solidity ^0.8.4; + +import '../extensions/IERC721ABatchBurnable.sol'; diff --git a/contracts/mocks/ERC721ABatchBurnableMock.sol b/contracts/mocks/ERC721ABatchBurnableMock.sol new file mode 100644 index 000000000..aa6dfc287 --- /dev/null +++ b/contracts/mocks/ERC721ABatchBurnableMock.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v4.2.3 +// Creators: Chiru Labs + +pragma solidity ^0.8.4; + +import '../extensions/ERC721ABatchBurnable.sol'; + +contract ERC721ABatchBurnableMock is ERC721ABatchBurnable { + constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {} + + function exists(uint256 tokenId) public view returns (bool) { + return _exists(tokenId); + } + + function safeMint(address to, uint256 quantity) public { + _safeMint(to, quantity); + } + + function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) { + return _ownershipAt(index); + } + + function totalMinted() public view returns (uint256) { + return _totalMinted(); + } + + function totalBurned() public view returns (uint256) { + return _totalBurned(); + } + + function numberBurned(address owner) public view returns (uint256) { + return _numberBurned(owner); + } + + function initializeOwnershipAt(uint256 index) public { + _initializeOwnershipAt(index); + } + + function batchBurnUnoptimized(uint256[] memory tokenIds) public { + unchecked { + uint256 tokenId; + for (uint256 i; i < tokenIds.length; ++i) { + tokenId = tokenIds[i]; + burn(tokenId); + } + } + } +} From 45d86b43b1cdd0f45681d852be56196d176df9bf Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 20 Feb 2023 02:27:53 +0100 Subject: [PATCH 37/74] fixed _batchBurn --- contracts/ERC721A.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 2c719e5fd..222ab7a4c 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1374,7 +1374,7 @@ contract ERC721A is IERC721A { _sort(tokenIds); uint256 totalTokens = tokenIds.length; - uint256 totalTokensLeft; + uint256 totalTokensLeft = totalTokens; uint256 startTokenId = tokenIds[0]; uint256 nextTokenId; uint256 prevOwnershipPacked = _packedOwnershipOf(startTokenId); From 8d0d63e36de6aa2272040677a5db6baff0712ce8 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 20 Feb 2023 03:46:28 +0100 Subject: [PATCH 38/74] fixed update of last tokenId + 1 --- contracts/ERC721A.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 222ab7a4c..085b9fc6f 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -792,9 +792,9 @@ contract ERC721A is IERC721A { i += quantity; } - // Update `nextTokenId + 1`, the tokenId subsequent to the last element in `tokenIds` + // Update the tokenId subsequent to the last element in `tokenIds` _updateNextTokenId( - nextTokenId + 1, + quantity == 1 ? startTokenId + 1 : nextTokenId + 1, quantity == 1 ? prevOwnershipPacked : nextOwnershipPacked, prevOwnershipPacked ); @@ -1469,9 +1469,9 @@ contract ERC721A is IERC721A { i += quantity; } - // Update `nextTokenId + 1`, the tokenId subsequent to the last element in `tokenIds` + // Update the tokenId subsequent to the last element in `tokenIds` _updateNextTokenId( - nextTokenId + 1, + quantity == 1 ? startTokenId + 1 : nextTokenId + 1, quantity == 1 ? prevOwnershipPacked : nextOwnershipPacked, prevOwnershipPacked ); From 4aab6e5da7596b7a0f073af5c2050eb9598c0199 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 20 Feb 2023 03:46:38 +0100 Subject: [PATCH 39/74] batchBurnable tests wip --- test/extensions/ERC721ABatchBurnable.test.js | 234 +++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 test/extensions/ERC721ABatchBurnable.test.js diff --git a/test/extensions/ERC721ABatchBurnable.test.js b/test/extensions/ERC721ABatchBurnable.test.js new file mode 100644 index 000000000..02308c7d0 --- /dev/null +++ b/test/extensions/ERC721ABatchBurnable.test.js @@ -0,0 +1,234 @@ +const { deployContract, getBlockTimestamp, mineBlockTimestamp, offsettedIndex } = require('../helpers.js'); +const { expect } = require('chai'); +const { constants } = require('@openzeppelin/test-helpers'); +const { ZERO_ADDRESS } = constants; + +const createTestSuite = ({ contract, constructorArgs }) => + function () { + let offsetted; + + context(`${contract}`, function () { + beforeEach(async function () { + this.erc721aBatchBurnable = await deployContract(contract, constructorArgs); + + this.startTokenId = 0; + + offsetted = (...arr) => offsettedIndex(this.startTokenId, arr); + }); + + beforeEach(async function () { + const [owner, addr1, addr2, spender] = await ethers.getSigners(); + this.owner = owner; + this.addr1 = addr1; + this.addr2 = addr2; + this.spender = spender; + this.numTestTokens = 20; + this.totalBurned = 6; + this.burnedTokenIds1 = [2, 3, 4]; + this.burnedTokenIds2 = [7, 10, 9]; + this.notBurnedTokenId1 = 1; + this.notBurnedTokenId2 = 5; + this.notBurnedTokenId3 = 6; + this.notBurnedTokenId4 = 8; + await this.erc721aBatchBurnable['safeMint(address,uint256)'](this.addr1.address, this.numTestTokens); + await this.erc721aBatchBurnable.connect(this.addr1).batchBurn(this.burnedTokenIds1); + await this.erc721aBatchBurnable.connect(this.addr1).batchBurn(this.burnedTokenIds2); + }); + + context('totalSupply()', function () { + it('has the expected value', async function () { + expect(await this.erc721aBatchBurnable.totalSupply()).to.equal(this.numTestTokens - this.totalBurned); + }); + + it('is reduced by burns', async function () { + const supplyBefore = await this.erc721aBatchBurnable.totalSupply(); + + await this.erc721aBatchBurnable + .connect(this.addr1) + .batchBurn(offsetted(this.notBurnedTokenId3, this.notBurnedTokenId4)); + + const supplyNow = await this.erc721aBatchBurnable.totalSupply(); + expect(supplyNow).to.equal(supplyBefore - 2); + }); + }); + + it('changes numberBurned', async function () { + expect(await this.erc721aBatchBurnable.numberBurned(this.addr1.address)).to.equal(this.totalBurned); + await this.erc721aBatchBurnable.connect(this.addr1).batchBurn([this.notBurnedTokenId4]); + expect(await this.erc721aBatchBurnable.numberBurned(this.addr1.address)).to.equal(this.totalBurned + 1); + }); + + it('changes totalBurned', async function () { + const totalBurnedBefore = (await this.erc721aBatchBurnable.totalBurned()).toNumber(); + + await this.erc721aBatchBurnable + .connect(this.addr1) + .batchBurn(offsetted(this.notBurnedTokenId3, this.notBurnedTokenId4)); + + const totalBurnedNow = (await this.erc721aBatchBurnable.totalBurned()).toNumber(); + expect(totalBurnedNow).to.equal(totalBurnedBefore + 2); + }); + + it('changes exists', async function () { + for (let i = 0; i < 3; ++i) { + expect(await this.erc721aBatchBurnable.exists(this.burnedTokenIds1[i])).to.be.false; + expect(await this.erc721aBatchBurnable.exists(this.burnedTokenIds2[i])).to.be.false; + } + + expect(await this.erc721aBatchBurnable.exists(this.notBurnedTokenId1)).to.be.true; + expect(await this.erc721aBatchBurnable.exists(this.notBurnedTokenId2)).to.be.true; + expect(await this.erc721aBatchBurnable.exists(this.notBurnedTokenId3)).to.be.true; + expect(await this.erc721aBatchBurnable.exists(this.notBurnedTokenId4)).to.be.true; + + await this.erc721aBatchBurnable + .connect(this.addr1) + .batchBurn(offsetted(this.notBurnedTokenId3, this.notBurnedTokenId4)); + expect(await this.erc721aBatchBurnable.exists(this.notBurnedTokenId3)).to.be.false; + expect(await this.erc721aBatchBurnable.exists(this.notBurnedTokenId4)).to.be.false; + expect(await this.erc721aBatchBurnable.exists(this.numTestTokens)).to.be.false; + }); + + it('cannot burn a non-existing token', async function () { + const query = this.erc721aBatchBurnable + .connect(this.addr1) + .batchBurn([this.notBurnedTokenId4, this.numTestTokens]); + await expect(query).to.be.revertedWith('OwnerQueryForNonexistentToken'); + }); + + it('cannot burn a burned token', async function () { + const query = this.erc721aBatchBurnable.connect(this.addr1).batchBurn(this.burnedTokenIds1); + await expect(query).to.be.revertedWith('OwnerQueryForNonexistentToken'); + }); + + it('cannot burn with wrong caller or spender', async function () { + const tokenIdsToBurn = [this.notBurnedTokenId1, this.notBurnedTokenId2]; + + // sanity check + await this.erc721aBatchBurnable.connect(this.addr1).approve(ZERO_ADDRESS, tokenIdsToBurn[0]); + await this.erc721aBatchBurnable.connect(this.addr1).approve(ZERO_ADDRESS, tokenIdsToBurn[1]); + await this.erc721aBatchBurnable.connect(this.addr1).setApprovalForAll(this.spender.address, false); + + const query = this.erc721aBatchBurnable.connect(this.spender).batchBurn(tokenIdsToBurn); + await expect(query).to.be.revertedWith('TransferCallerNotOwnerNorApproved'); + }); + + it('spender can burn with specific approved tokenId', async function () { + const tokenIdsToBurn = [this.notBurnedTokenId1, this.notBurnedTokenId2]; + + await this.erc721aBatchBurnable.connect(this.addr1).approve(this.spender.address, tokenIdsToBurn[0]); + await this.erc721aBatchBurnable.connect(this.addr1).approve(this.spender.address, tokenIdsToBurn[1]); + await this.erc721aBatchBurnable.connect(this.spender).batchBurn(tokenIdsToBurn); + expect(await this.erc721aBatchBurnable.exists(tokenIdsToBurn[0])).to.be.false; + expect(await this.erc721aBatchBurnable.exists(tokenIdsToBurn[1])).to.be.false; + }); + + it('spender can burn with one-time approval', async function () { + const tokenIdsToBurn = [this.notBurnedTokenId1, this.notBurnedTokenId2]; + + await this.erc721aBatchBurnable.connect(this.addr1).setApprovalForAll(this.spender.address, true); + await this.erc721aBatchBurnable.connect(this.spender).batchBurn(tokenIdsToBurn); + expect(await this.erc721aBatchBurnable.exists(tokenIdsToBurn[0])).to.be.false; + expect(await this.erc721aBatchBurnable.exists(tokenIdsToBurn[1])).to.be.false; + }); + + it('cannot transfer a burned token', async function () { + const query = this.erc721aBatchBurnable + .connect(this.addr1) + .transferFrom(this.addr1.address, this.addr2.address, this.burnedTokenIds1[0]); + await expect(query).to.be.revertedWith('OwnerQueryForNonexistentToken'); + }); + + it('does not affect _totalMinted', async function () { + const tokenIdsToBurn = [this.notBurnedTokenId1, this.notBurnedTokenId2]; + const totalMintedBefore = await this.erc721aBatchBurnable.totalMinted(); + expect(totalMintedBefore).to.equal(this.numTestTokens); + await this.erc721aBatchBurnable.connect(this.addr1).batchBurn(tokenIdsToBurn); + expect(await this.erc721aBatchBurnable.totalMinted()).to.equal(totalMintedBefore); + }); + + it('adjusts owners balances', async function () { + expect(await this.erc721aBatchBurnable.balanceOf(this.addr1.address)).to.be.equal( + this.numTestTokens - this.totalBurned + ); + }); + + it('startTimestamp updated correctly', async function () { + const tokenIdsToBurn = [this.notBurnedTokenId1]; + const ownershipBefore = await this.erc721aBatchBurnable.getOwnershipAt(tokenIdsToBurn[0]); + const timestampBefore = parseInt(ownershipBefore.startTimestamp); + const timestampToMine = (await getBlockTimestamp()) + 12345; + await mineBlockTimestamp(timestampToMine); + const timestampMined = await getBlockTimestamp(); + await this.erc721aBatchBurnable.connect(this.addr1).batchBurn(tokenIdsToBurn); + const ownershipAfter = await this.erc721aBatchBurnable.getOwnershipAt(tokenIdsToBurn[0]); + const timestampAfter = parseInt(ownershipAfter.startTimestamp); + expect(timestampBefore).to.be.lt(timestampToMine); + expect(timestampAfter).to.be.gte(timestampToMine); + expect(timestampAfter).to.be.lt(timestampToMine + 10); + expect(timestampToMine).to.be.eq(timestampMined); + }); + + describe('ownerships correctly set', async function () { + it('with token before previously burnt token transferred and burned', async function () { + await this.erc721aBatchBurnable + .connect(this.addr1) + .transferFrom(this.addr1.address, this.addr2.address, this.notBurnedTokenId1); + expect(await this.erc721aBatchBurnable.ownerOf(this.notBurnedTokenId1)).to.be.equal(this.addr2.address); + await this.erc721aBatchBurnable.connect(this.addr2).batchBurn([this.notBurnedTokenId1]); + for (let i = 0; i < this.numTestTokens; ++i) { + if (i == this.notBurnedTokenId1 || this.burnedTokenIds1.includes(i) || this.burnedTokenIds2.includes(i)) { + await expect(this.erc721aBatchBurnable.ownerOf(i)).to.be.revertedWith('OwnerQueryForNonexistentToken'); + } else { + expect(await this.erc721aBatchBurnable.ownerOf(i)).to.be.equal(this.addr1.address); + } + } + }); + + it('with token after previously burnt token transferred and burned', async function () { + const tokenIdsToBurn = [this.notBurnedTokenId1, this.notBurnedTokenId3]; + await this.erc721aBatchBurnable + .connect(this.addr1) + .transferFrom(this.addr1.address, this.addr2.address, tokenIdsToBurn[0]); + await this.erc721aBatchBurnable + .connect(this.addr1) + .transferFrom(this.addr1.address, this.addr2.address, tokenIdsToBurn[1]); + expect(await this.erc721aBatchBurnable.ownerOf(tokenIdsToBurn[0])).to.be.equal(this.addr2.address); + expect(await this.erc721aBatchBurnable.ownerOf(tokenIdsToBurn[1])).to.be.equal(this.addr2.address); + await this.erc721aBatchBurnable.connect(this.addr2).batchBurn(tokenIdsToBurn); + for (let i = 0; i < this.numTestTokens; ++i) { + if (tokenIdsToBurn.includes(i) || this.burnedTokenIds1.includes(i) || this.burnedTokenIds2.includes(i)) { + await expect(this.erc721aBatchBurnable.ownerOf(i)).to.be.revertedWith('OwnerQueryForNonexistentToken'); + } else { + expect(await this.erc721aBatchBurnable.ownerOf(i)).to.be.equal(this.addr1.address); + } + } + }); + + it('with first token burned', async function () { + await this.erc721aBatchBurnable.connect(this.addr1).batchBurn([0]); + for (let i = 0; i < this.numTestTokens; ++i) { + if (i == 0 || this.burnedTokenIds1.includes(i) || this.burnedTokenIds2.includes(i)) { + await expect(this.erc721aBatchBurnable.ownerOf(i)).to.be.revertedWith('OwnerQueryForNonexistentToken'); + } else { + expect(await this.erc721aBatchBurnable.ownerOf(i)).to.be.equal(this.addr1.address); + } + } + }); + + it('with last token burned', async function () { + await expect(this.erc721aBatchBurnable.ownerOf(offsetted(this.numTestTokens))).to.be.revertedWith( + 'OwnerQueryForNonexistentToken' + ); + await this.erc721aBatchBurnable.connect(this.addr1).batchBurn([offsetted(this.numTestTokens - 1)]); + await expect(this.erc721aBatchBurnable.ownerOf(offsetted(this.numTestTokens - 1))).to.be.revertedWith( + 'OwnerQueryForNonexistentToken' + ); + }); + }); + }); + }; + +describe( + 'ERC721ABatchBurnable', + createTestSuite({ contract: 'ERC721ABatchBurnableMock', constructorArgs: ['Azuki', 'AZUKI'] }) +); From 6c5c73088bd77aa28f0a7baac88d7a097b2d08c5 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 20 Feb 2023 03:53:08 +0100 Subject: [PATCH 40/74] refactor --- contracts/ERC721A.sol | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 085b9fc6f..321162504 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -793,11 +793,11 @@ contract ERC721A is IERC721A { } // Update the tokenId subsequent to the last element in `tokenIds` - _updateNextTokenId( - quantity == 1 ? startTokenId + 1 : nextTokenId + 1, - quantity == 1 ? prevOwnershipPacked : nextOwnershipPacked, - prevOwnershipPacked - ); + if (quantity == 1) { + _updateNextTokenId(startTokenId + 1, prevOwnershipPacked, prevOwnershipPacked); + } else { + _updateNextTokenId(nextTokenId + 1, nextOwnershipPacked, prevOwnershipPacked); + } } _afterTokenBatchTransfers(from, to, tokenIds); @@ -1470,11 +1470,11 @@ contract ERC721A is IERC721A { } // Update the tokenId subsequent to the last element in `tokenIds` - _updateNextTokenId( - quantity == 1 ? startTokenId + 1 : nextTokenId + 1, - quantity == 1 ? prevOwnershipPacked : nextOwnershipPacked, - prevOwnershipPacked - ); + if (quantity == 1) { + _updateNextTokenId(startTokenId + 1, prevOwnershipPacked, prevOwnershipPacked); + } else { + _updateNextTokenId(nextTokenId + 1, nextOwnershipPacked, prevOwnershipPacked); + } // Overflow not possible, as _burnCounter cannot be exceed _currentIndex times. _burnCounter += totalTokens; From 7357f341ec34d9f5d0437f13012aaf45a3f7a3eb Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 20 Feb 2023 13:49:58 +0100 Subject: [PATCH 41/74] fix --- contracts/ERC721A.sol | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 321162504..8179a0120 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -725,7 +725,7 @@ contract ERC721A is IERC721A { startTokenId = tokenIds[i]; totalTokensLeft = totalTokens - i; - // Check ownership of `startTokenId`. + // Update `prevOwnershipPacked` and check ownership of `startTokenId`. prevOwnershipPacked = _packedOwnershipOf(startTokenId); if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); @@ -1366,6 +1366,7 @@ contract ERC721A is IERC721A { * Requirements: * * - `tokenIds` must exist. + * - `tokenIds` must be owned by the same address. * * Emits a {Transfer} event for each burn. */ @@ -1409,8 +1410,9 @@ contract ERC721A is IERC721A { startTokenId = tokenIds[i]; totalTokensLeft = totalTokens - i; - // Check ownership of `startTokenId`. + // Update `prevOwnershipPacked` and check ownership of `startTokenId`. prevOwnershipPacked = _packedOwnershipOf(startTokenId); + if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); } // Updates startTokenId: @@ -1457,8 +1459,16 @@ contract ERC721A is IERC721A { // Update `prevOwnershipPacked` with last initialized `nextOwnershipPacked`. prevOwnershipPacked = nextOwnershipPacked; - // Clear the slot to leverage consecutive burns optimization. - delete _packedOwnerships[nextTokenId]; + // Updates nextTokenId: + // - `address` to the last owner. + // - `startTimestamp` to the timestamp of burning. + // - `burned` to `true`. + // - `nextInitialized` is left unchanged. + _packedOwnerships[nextTokenId] = _packOwnershipData( + from, + (_BITMASK_BURNED | (nextOwnershipPacked & _BITMASK_NEXT_INITIALIZED)) | + _nextExtraData(from, address(0), nextOwnershipPacked) + ); } // Clear approvals and emit transfer event for `nextTokenId`. From 35813a70dbc84adb5d1dc780473cd4f1293867a3 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 20 Feb 2023 14:14:45 +0100 Subject: [PATCH 42/74] add auto-clearing of consecutive ids and set `nextInitialized` to false --- contracts/ERC721A.sol | 77 ++++++++++--------- .../ERC721ABatchTransferable.test.js | 6 +- 2 files changed, 47 insertions(+), 36 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 8179a0120..003ea2de5 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -729,15 +729,12 @@ contract ERC721A is IERC721A { prevOwnershipPacked = _packedOwnershipOf(startTokenId); if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); - // Updates startTokenId: + // Update startTokenId: // - `address` to the next owner. // - `startTimestamp` to the timestamp of transfering. // - `burned` to `false`. - // - `nextInitialized` is left unchanged. - _packedOwnerships[startTokenId] = _packOwnershipData( - to, - (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | _nextExtraData(from, to, prevOwnershipPacked) - ); + // - `nextInitialized` to `false`, to account for potential clearing of `startTokenId + 1`. + _packedOwnerships[startTokenId] = _packOwnershipData(to, _nextExtraData(from, to, prevOwnershipPacked)); // Clear approvals and emit transfer event for `startTokenId`. _clearApprovalsAndEmitTransferEvent(from, toMasked, startTokenId, approvalCheck); @@ -769,19 +766,25 @@ contract ERC721A is IERC721A { if (address(uint160(nextOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); if (nextOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); - // Update `prevOwnershipPacked` with last initialized `nextOwnershipPacked`. - prevOwnershipPacked = nextOwnershipPacked; - - // Updates nextTokenId: - // - `address` to the next owner. - // - `startTimestamp` to the timestamp of transfering. - // - `burned` to `false`. - // - `nextInitialized` is left unchanged. - _packedOwnerships[nextTokenId] = _packOwnershipData( - to, - (nextOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | - _nextExtraData(from, to, nextOwnershipPacked) - ); + // If there is extra data to preserve + if (nextOwnershipPacked >> _BITPOS_EXTRA_DATA != 0) { + // Update `prevOwnershipPacked` with last initialized `nextOwnershipPacked`. + prevOwnershipPacked = nextOwnershipPacked; + + // Update `nextTokenId` + // - `address` to the next owner. + // - `startTimestamp` to the timestamp of transfering. + // - `burned` to `false`. + // - `nextInitialized` to `false`, to account for potential clearing of `nextTokenId + 1`. + _packedOwnerships[nextTokenId] = _packOwnershipData( + to, + (nextOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | + _nextExtraData(from, to, nextOwnershipPacked) + ); + } else { + // Otherwise clear slot of `nextTokenId` to leverage gas optimization of consecutive ids + delete _packedOwnerships[nextTokenId]; + } } // Clear approvals and emit transfer event for `nextTokenId`. @@ -1419,11 +1422,10 @@ contract ERC721A is IERC721A { // - `address` to the last owner. // - `startTimestamp` to the timestamp of burning. // - `burned` to `true`. - // - `nextInitialized` is left unchanged. + // - `nextInitialized` to `false`, to account for potential clearing of `startTokenId + 1`. _packedOwnerships[startTokenId] = _packOwnershipData( from, - (_BITMASK_BURNED | (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED)) | - _nextExtraData(from, address(0), prevOwnershipPacked) + _BITMASK_BURNED | _nextExtraData(from, address(0), prevOwnershipPacked) ); // Clear approvals and emit transfer event for `startTokenId`. @@ -1456,19 +1458,24 @@ contract ERC721A is IERC721A { if (address(uint160(nextOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); if (nextOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); - // Update `prevOwnershipPacked` with last initialized `nextOwnershipPacked`. - prevOwnershipPacked = nextOwnershipPacked; - - // Updates nextTokenId: - // - `address` to the last owner. - // - `startTimestamp` to the timestamp of burning. - // - `burned` to `true`. - // - `nextInitialized` is left unchanged. - _packedOwnerships[nextTokenId] = _packOwnershipData( - from, - (_BITMASK_BURNED | (nextOwnershipPacked & _BITMASK_NEXT_INITIALIZED)) | - _nextExtraData(from, address(0), nextOwnershipPacked) - ); + // If there is extra data to preserve + if (nextOwnershipPacked >> _BITPOS_EXTRA_DATA != 0) { + // Update `prevOwnershipPacked` with last initialized `nextOwnershipPacked`. + prevOwnershipPacked = nextOwnershipPacked; + + // Update `nextTokenId` + // - `address` to the last owner. + // - `startTimestamp` to the timestamp of burning. + // - `burned` to `true`. + // - `nextInitialized` to `false`, to account for potential clearing of `nextTokenId + 1`. + _packedOwnerships[nextTokenId] = _packOwnershipData( + from, + _BITMASK_BURNED | _nextExtraData(from, address(0), nextOwnershipPacked) + ); + } else { + // Otherwise clear slot of `nextTokenId` to leverage gas optimization of consecutive ids + delete _packedOwnerships[nextTokenId]; + } } // Clear approvals and emit transfer event for `nextTokenId`. diff --git a/test/extensions/ERC721ABatchTransferable.test.js b/test/extensions/ERC721ABatchTransferable.test.js index 1e018c689..4bf8c7b6c 100644 --- a/test/extensions/ERC721ABatchTransferable.test.js +++ b/test/extensions/ERC721ABatchTransferable.test.js @@ -103,7 +103,11 @@ const createTestSuite = ({ contract, constructorArgs }) => // Initialized tokens were updated expect((await this.erc721aBatchTransferable.getOwnershipAt(3))[0]).to.be.equal(this.to.address); - expect((await this.erc721aBatchTransferable.getOwnershipAt(8))[0]).to.be.equal(this.to.address); + + // Initialized tokens in a consecutive transfer are cleared + expect((await this.erc721aBatchTransferable.getOwnershipAt(8))[0]).to.be.equal( + transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.to.address + ); // Uninitialized tokens are left uninitialized expect((await this.erc721aBatchTransferable.getOwnershipAt(7))[0]).to.be.equal( From ddc6bd45175defb6414a5a54ab90b5a4281f0c86 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 20 Feb 2023 14:38:44 +0100 Subject: [PATCH 43/74] batchTransfer tests refactor --- .../ERC721ABatchTransferable.test.js | 444 ++++++++---------- 1 file changed, 200 insertions(+), 244 deletions(-) diff --git a/test/extensions/ERC721ABatchTransferable.test.js b/test/extensions/ERC721ABatchTransferable.test.js index 4bf8c7b6c..8d192db48 100644 --- a/test/extensions/ERC721ABatchTransferable.test.js +++ b/test/extensions/ERC721ABatchTransferable.test.js @@ -59,334 +59,290 @@ const createTestSuite = ({ contract, constructorArgs }) => context('test batch transfer functionality', function () { const testSuccessfulBatchTransfer = function (transferFn, transferToContract = true) { - beforeEach(async function () { - const sender = this.addr2; - this.tokenIds = this.addr2.expected.tokens; - this.from = sender.address; - this.to = transferToContract ? this.receiver : this.addr4; - this.approvedIds = [this.tokenIds[0], this.tokenIds[2], this.tokenIds[3]]; - - this.approvedIds.forEach(async (tokenId) => { - await this.erc721aBatchTransferable.connect(sender).approve(this.to.address, tokenId); - }); - - const ownershipBefore = await this.erc721aBatchTransferable.getOwnershipAt(this.tokenIds[0]); - this.timestampBefore = parseInt(ownershipBefore.startTimestamp); - this.timestampToMine = (await getBlockTimestamp()) + 12345; - await mineBlockTimestamp(this.timestampToMine); - this.timestampMined = await getBlockTimestamp(); - - // Manually initialize some tokens of addr2 - await this.erc721aBatchTransferable.initializeOwnershipAt(3); - await this.erc721aBatchTransferable.initializeOwnershipAt(8); - - // prettier-ignore - this.transferTx = await this.erc721aBatchTransferable + describe('successful transfers', async function () { + beforeEach(async function () { + const sender = this.addr2; + this.tokenIds = this.addr2.expected.tokens; + this.from = sender.address; + this.to = transferToContract ? this.receiver : this.addr4; + this.approvedIds = [this.tokenIds[0], this.tokenIds[2], this.tokenIds[3]]; + + this.approvedIds.forEach(async (tokenId) => { + await this.erc721aBatchTransferable.connect(sender).approve(this.to.address, tokenId); + }); + + const ownershipBefore = await this.erc721aBatchTransferable.getOwnershipAt(this.tokenIds[0]); + this.timestampBefore = parseInt(ownershipBefore.startTimestamp); + this.timestampToMine = (await getBlockTimestamp()) + 12345; + await mineBlockTimestamp(this.timestampToMine); + this.timestampMined = await getBlockTimestamp(); + + // Manually initialize some tokens of addr2 + await this.erc721aBatchTransferable.initializeOwnershipAt(3); + await this.erc721aBatchTransferable.initializeOwnershipAt(8); + + // prettier-ignore + this.transferTx = await this.erc721aBatchTransferable .connect(sender)[transferFn](this.from, this.to.address, this.tokenIds); - // Transfer part of uninitialized tokens - this.tokensToTransferAlt = [25, 26, 27]; - // prettier-ignore - this.transferTxAlt = await this.erc721aBatchTransferable.connect(this.addr3)[transferFn]( + // Transfer part of uninitialized tokens + this.tokensToTransferAlt = [25, 26, 27]; + // prettier-ignore + this.transferTxAlt = await this.erc721aBatchTransferable.connect(this.addr3)[transferFn]( this.addr3.address, this.addr5.address, this.tokensToTransferAlt ); - const ownershipAfter = await this.erc721aBatchTransferable.getOwnershipAt(this.tokenIds[0]); - this.timestampAfter = parseInt(ownershipAfter.startTimestamp); - }); - - it('transfers the ownership of the given token IDs to the given address', async function () { - for (let i = 0; i < this.tokenIds.length; i++) { - const tokenId = this.tokenIds[i]; - expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.to.address); - } - - // Initialized tokens were updated - expect((await this.erc721aBatchTransferable.getOwnershipAt(3))[0]).to.be.equal(this.to.address); - - // Initialized tokens in a consecutive transfer are cleared - expect((await this.erc721aBatchTransferable.getOwnershipAt(8))[0]).to.be.equal( - transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.to.address - ); - - // Uninitialized tokens are left uninitialized - expect((await this.erc721aBatchTransferable.getOwnershipAt(7))[0]).to.be.equal( - transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.to.address - ); - - // Other tokens in between are left unchanged - for (let i = 0; i < this.addr1.expected.tokens.length; i++) { - const tokenId = this.addr1.expected.tokens[i]; - expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr1.address); - } - }); - - it('transfers the ownership of uninitialized token IDs to the given address', async function () { - const allTokensInitiallyOwned = this.addr3.expected.tokens; - allTokensInitiallyOwned.splice(2, this.tokensToTransferAlt.length); - - for (let i = 0; i < this.tokensToTransferAlt.length; i++) { - const tokenId = this.tokensToTransferAlt[i]; - expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr5.address); - } - - for (let i = 0; i < allTokensInitiallyOwned.length; i++) { - const tokenId = allTokensInitiallyOwned[i]; - expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr3.address); - } - - // Ownership of tokens was updated - expect((await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[0]))[0]).to.be.equal( - this.addr5.address - ); - expect((await this.erc721aBatchTransferable.getOwnershipAt(allTokensInitiallyOwned[2]))[0]).to.be.equal( - this.addr3.address - ); + const ownershipAfter = await this.erc721aBatchTransferable.getOwnershipAt(this.tokenIds[0]); + this.timestampAfter = parseInt(ownershipAfter.startTimestamp); + }); - // Uninitialized tokens are left uninitialized - expect( - (await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[0] - 1))[0] - ).to.be.equal(ZERO_ADDRESS); - expect((await this.erc721aBatchTransferable.getOwnershipAt(allTokensInitiallyOwned[3]))[0]).to.be.equal( - ZERO_ADDRESS - ); - expect((await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[1]))[0]).to.be.equal( - transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.addr5.address - ); - expect((await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[2]))[0]).to.be.equal( - transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.addr5.address - ); - }); + it('emits Transfers event', async function () { + for (let i = 0; i < this.tokenIds.length; i++) { + const tokenId = this.tokenIds[i]; + await expect(this.transferTx) + .to.emit(this.erc721aBatchTransferable, 'Transfer') + .withArgs(this.from, this.to.address, tokenId); + } + }); - it('emits Transfers event', async function () { - for (let i = 0; i < this.tokenIds.length; i++) { - const tokenId = this.tokenIds[i]; - await expect(this.transferTx) - .to.emit(this.erc721aBatchTransferable, 'Transfer') - .withArgs(this.from, this.to.address, tokenId); - } - }); + it('adjusts owners balances', async function () { + expect(await this.erc721aBatchTransferable.balanceOf(this.from)).to.be.equal(0); + expect(await this.erc721aBatchTransferable.balanceOf(this.to.address)).to.be.equal( + this.addr2.expected.mintCount + ); + expect(await this.erc721aBatchTransferable.balanceOf(this.addr3.address)).to.be.equal( + this.addr3.expected.tokens.length - this.tokensToTransferAlt.length + ); + expect(await this.erc721aBatchTransferable.balanceOf(this.addr5.address)).to.be.equal( + this.tokensToTransferAlt.length + ); + }); - it('clears the approval for the token IDs', async function () { - this.approvedIds.forEach(async (tokenId) => { - expect(await this.erc721aBatchTransferable.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS); + it('clears the approval for the token IDs', async function () { + this.approvedIds.forEach(async (tokenId) => { + expect(await this.erc721aBatchTransferable.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS); + }); }); - }); - it('adjusts owners balances', async function () { - expect(await this.erc721aBatchTransferable.balanceOf(this.from)).to.be.equal(0); - expect(await this.erc721aBatchTransferable.balanceOf(this.to.address)).to.be.equal( - this.addr2.expected.mintCount - ); - expect(await this.erc721aBatchTransferable.balanceOf(this.addr3.address)).to.be.equal( - this.addr3.expected.tokens.length - this.tokensToTransferAlt.length - ); - expect(await this.erc721aBatchTransferable.balanceOf(this.addr5.address)).to.be.equal( - this.tokensToTransferAlt.length - ); - }); + it('startTimestamp updated correctly', async function () { + expect(this.timestampBefore).to.be.lt(this.timestampToMine); + expect(this.timestampAfter).to.be.gte(this.timestampToMine); + expect(this.timestampAfter).to.be.lt(this.timestampToMine + 10); + expect(this.timestampToMine).to.be.eq(this.timestampMined); + }); - it('startTimestamp updated correctly', async function () { - expect(this.timestampBefore).to.be.lt(this.timestampToMine); - expect(this.timestampAfter).to.be.gte(this.timestampToMine); - expect(this.timestampAfter).to.be.lt(this.timestampToMine + 10); - expect(this.timestampToMine).to.be.eq(this.timestampMined); + describe('ownership correctly set', async function () { + it('with transfer of the given token IDs to the given address', async function () { + for (let i = 0; i < this.tokenIds.length; i++) { + const tokenId = this.tokenIds[i]; + expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.to.address); + } + + // Initialized tokens were updated + expect((await this.erc721aBatchTransferable.getOwnershipAt(3))[0]).to.be.equal(this.to.address); + + // Initialized tokens in a consecutive transfer are cleared + expect((await this.erc721aBatchTransferable.getOwnershipAt(8))[0]).to.be.equal( + transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.to.address + ); + + // Uninitialized tokens are left uninitialized + expect((await this.erc721aBatchTransferable.getOwnershipAt(7))[0]).to.be.equal( + transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.to.address + ); + + // Other tokens in between are left unchanged + for (let i = 0; i < this.addr1.expected.tokens.length; i++) { + const tokenId = this.addr1.expected.tokens[i]; + expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr1.address); + } + }); + + it('with transfers of uninitialized token IDs to the given address', async function () { + const allTokensInitiallyOwned = this.addr3.expected.tokens; + allTokensInitiallyOwned.splice(2, this.tokensToTransferAlt.length); + + for (let i = 0; i < this.tokensToTransferAlt.length; i++) { + const tokenId = this.tokensToTransferAlt[i]; + expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr5.address); + } + + for (let i = 0; i < allTokensInitiallyOwned.length; i++) { + const tokenId = allTokensInitiallyOwned[i]; + expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr3.address); + } + + // Ownership of tokens was updated + expect( + (await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[0]))[0] + ).to.be.equal(this.addr5.address); + expect((await this.erc721aBatchTransferable.getOwnershipAt(allTokensInitiallyOwned[2]))[0]).to.be.equal( + this.addr3.address + ); + + // Uninitialized tokens are left uninitialized + expect( + (await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[0] - 1))[0] + ).to.be.equal(ZERO_ADDRESS); + expect((await this.erc721aBatchTransferable.getOwnershipAt(allTokensInitiallyOwned[3]))[0]).to.be.equal( + ZERO_ADDRESS + ); + expect( + (await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[1]))[0] + ).to.be.equal(transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.addr5.address); + expect( + (await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[2]))[0] + ).to.be.equal(transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.addr5.address); + }); + }); }); }; const testUnsuccessfulBatchTransfer = function (transferFn) { - beforeEach(function () { - this.tokenIds = this.addr2.expected.tokens.slice(0, 2); - this.sender = this.addr1; - }); + describe('unsuccessful transfers', function () { + beforeEach(function () { + this.tokenIds = this.addr2.expected.tokens.slice(0, 2); + this.sender = this.addr1; + }); - it('rejects unapproved transfer', async function () { - // prettier-ignore - await expect( + it('rejects unapproved transfer', async function () { + // prettier-ignore + await expect( this.erc721aBatchTransferable .connect(this.sender)[transferFn]( this.addr2.address, this.sender.address, this.tokenIds ) ).to.be.revertedWith('TransferCallerNotOwnerNorApproved'); - }); + }); - it('rejects transfer from incorrect owner', async function () { - await this.erc721aBatchTransferable.connect(this.addr2).setApprovalForAll(this.sender.address, true); - // prettier-ignore - await expect( + it('rejects transfer from incorrect owner', async function () { + await this.erc721aBatchTransferable.connect(this.addr2).setApprovalForAll(this.sender.address, true); + // prettier-ignore + await expect( this.erc721aBatchTransferable .connect(this.sender)[transferFn]( this.addr3.address, this.sender.address, this.tokenIds ) ).to.be.revertedWith('TransferFromIncorrectOwner'); - }); + }); - it('rejects transfer to zero address', async function () { - await this.erc721aBatchTransferable.connect(this.addr2).setApprovalForAll(this.sender.address, true); - // prettier-ignore - await expect( + it('rejects transfer to zero address', async function () { + await this.erc721aBatchTransferable.connect(this.addr2).setApprovalForAll(this.sender.address, true); + // prettier-ignore + await expect( this.erc721aBatchTransferable .connect(this.sender)[transferFn]( this.addr2.address, ZERO_ADDRESS, this.tokenIds ) ).to.be.revertedWith('TransferToZeroAddress'); + }); }); }; const testApproveBatchTransfer = function (transferFn) { - beforeEach(function () { - this.tokenIds = this.addr1.expected.tokens.slice(0, 2); - }); + describe('approvals correctly set', async function () { + beforeEach(function () { + this.tokenIds = this.addr1.expected.tokens.slice(0, 2); + }); - it('approval allows batch transfers', async function () { - // prettier-ignore - await expect( + it('approval allows batch transfers', async function () { + // prettier-ignore + await expect( this.erc721aBatchTransferable .connect(this.addr3)[transferFn]( this.addr1.address, this.addr3.address, this.tokenIds ) ).to.be.revertedWith('TransferCallerNotOwnerNorApproved'); - for (let i = 0; i < this.tokenIds.length; i++) { - const tokenId = this.tokenIds[i]; - await this.erc721aBatchTransferable.connect(this.addr1).approve(this.addr3.address, tokenId); - } + for (let i = 0; i < this.tokenIds.length; i++) { + const tokenId = this.tokenIds[i]; + await this.erc721aBatchTransferable.connect(this.addr1).approve(this.addr3.address, tokenId); + } - // prettier-ignore - await this.erc721aBatchTransferable + // prettier-ignore + await this.erc721aBatchTransferable .connect(this.addr3)[transferFn]( this.addr1.address, this.addr3.address, this.tokenIds ); - // prettier-ignore - await expect( + // prettier-ignore + await expect( this.erc721aBatchTransferable .connect(this.addr1)[transferFn]( this.addr3.address, this.addr1.address, this.tokenIds ) ).to.be.revertedWith('TransferCallerNotOwnerNorApproved'); - }); + }); - it('self-approval is cleared on batch transfers', async function () { - for (let i = 0; i < this.tokenIds.length; i++) { - const tokenId = this.tokenIds[i]; - await this.erc721aBatchTransferable.connect(this.addr1).approve(this.addr1.address, tokenId); - expect(await this.erc721aBatchTransferable.getApproved(tokenId)).to.equal(this.addr1.address); - } + it('self-approval is cleared on batch transfers', async function () { + for (let i = 0; i < this.tokenIds.length; i++) { + const tokenId = this.tokenIds[i]; + await this.erc721aBatchTransferable.connect(this.addr1).approve(this.addr1.address, tokenId); + expect(await this.erc721aBatchTransferable.getApproved(tokenId)).to.equal(this.addr1.address); + } - // prettier-ignore - await this.erc721aBatchTransferable + // prettier-ignore + await this.erc721aBatchTransferable .connect(this.addr1)[transferFn]( this.addr1.address, this.addr2.address, this.tokenIds ); - for (let i = 0; i < this.tokenIds.length; i++) { - const tokenId = this.tokenIds[i]; - expect(await this.erc721aBatchTransferable.getApproved(tokenId)).to.not.equal(this.addr1.address); - } - }); + for (let i = 0; i < this.tokenIds.length; i++) { + const tokenId = this.tokenIds[i]; + expect(await this.erc721aBatchTransferable.getApproved(tokenId)).to.not.equal(this.addr1.address); + } + }); - it('approval for all allows batch transfers', async function () { - await this.erc721aBatchTransferable.connect(this.addr1).setApprovalForAll(this.addr3.address, true); + it('approval for all allows batch transfers', async function () { + await this.erc721aBatchTransferable.connect(this.addr1).setApprovalForAll(this.addr3.address, true); - // prettier-ignore - await this.erc721aBatchTransferable + // prettier-ignore + await this.erc721aBatchTransferable .connect(this.addr3)[transferFn]( this.addr1.address, this.addr3.address, this.tokenIds ); + }); }); }; context('successful transfers', function () { - context('batchTransferFrom', function () { - describe('to contract', function () { - testSuccessfulBatchTransfer('batchTransferFrom'); - }); - - describe('to EOA', function () { - testSuccessfulBatchTransfer('batchTransferFrom', false); - }); - }); - context('safeBatchTransferFrom', function () { - describe('to contract', function () { - testSuccessfulBatchTransfer('safeBatchTransferFrom(address,address,uint256[])'); - }); - - describe('to EOA', function () { - testSuccessfulBatchTransfer('safeBatchTransferFrom(address,address,uint256[])', false); - }); - }); - - // TEMPORARY: to use as comparison for gas usage - context('batchTransferFromUnoptimized', function () { - describe('to contract', function () { - testSuccessfulBatchTransfer('batchTransferFromUnoptimized'); - }); - - describe('to EOA', function () { - testSuccessfulBatchTransfer('batchTransferFromUnoptimized', false); - }); - }); - }); - - context('unsuccessful transfers', function () { - context('batchTransferFrom', function () { - describe('to contract', function () { - testUnsuccessfulBatchTransfer('batchTransferFrom'); - }); - - describe('to EOA', function () { - testUnsuccessfulBatchTransfer('batchTransferFrom', false); - }); - }); - context('safeBatchTransferFrom', function () { - describe('to contract', function () { - testUnsuccessfulBatchTransfer('safeBatchTransferFrom(address,address,uint256[])'); - }); - - describe('to EOA', function () { - testUnsuccessfulBatchTransfer('safeBatchTransferFrom(address,address,uint256[])', false); - }); - }); - - // TEMPORARY: to use as comparison for gas usage - context('batchTransferFromUnoptimized', function () { - describe('to contract', function () { - testUnsuccessfulBatchTransfer('batchTransferFromUnoptimized'); - }); - - describe('to EOA', function () { - testUnsuccessfulBatchTransfer('batchTransferFromUnoptimized', false); - }); - }); - }); - - context('approvals', function () { - context('batchTransferFrom', function () { + context('batchTransferFrom', function (fn = 'batchTransferFrom') { describe('to contract', function () { - testApproveBatchTransfer('batchTransferFrom'); + testSuccessfulBatchTransfer(fn); + testUnsuccessfulBatchTransfer(fn); + testApproveBatchTransfer(fn); }); describe('to EOA', function () { - testApproveBatchTransfer('batchTransferFrom', false); + testSuccessfulBatchTransfer(fn, false); + testUnsuccessfulBatchTransfer(fn, false); + testApproveBatchTransfer(fn, false); }); }); - context('safeBatchTransferFrom', function () { + context('safeBatchTransferFrom', function (fn = 'safeBatchTransferFrom(address,address,uint256[])') { describe('to contract', function () { - testApproveBatchTransfer('safeBatchTransferFrom(address,address,uint256[])'); + testSuccessfulBatchTransfer(fn); + testUnsuccessfulBatchTransfer(fn); + testApproveBatchTransfer(fn); }); describe('to EOA', function () { - testApproveBatchTransfer('safeBatchTransferFrom(address,address,uint256[])', false); + testSuccessfulBatchTransfer(fn, false); + testUnsuccessfulBatchTransfer(fn, false); + testApproveBatchTransfer(fn, false); }); }); - // TEMPORARY: to use as comparison for gas usage - context('batchTransferFromUnoptimized', function () { + // Use to compare gas usage and verify expected behaviour with respect to normal transfers + context('batchTransferFromUnoptimized', function (fn = 'batchTransferFromUnoptimized') { describe('to contract', function () { - testApproveBatchTransfer('batchTransferFromUnoptimized'); + testSuccessfulBatchTransfer(fn); + testUnsuccessfulBatchTransfer(fn); + testApproveBatchTransfer(fn); }); describe('to EOA', function () { - testApproveBatchTransfer('batchTransferFromUnoptimized', false); + testSuccessfulBatchTransfer(fn, false); + testUnsuccessfulBatchTransfer(fn, false); + testApproveBatchTransfer(fn, false); }); }); }); From 091870d1ecb867ea6bb9fcd7fb8921cf594c7642 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 20 Feb 2023 15:23:01 +0100 Subject: [PATCH 44/74] tests wip --- .../ERC721ABatchTransferable.test.js | 61 +++++++++++++++---- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/test/extensions/ERC721ABatchTransferable.test.js b/test/extensions/ERC721ABatchTransferable.test.js index 8d192db48..29ec93f39 100644 --- a/test/extensions/ERC721ABatchTransferable.test.js +++ b/test/extensions/ERC721ABatchTransferable.test.js @@ -41,7 +41,7 @@ const createTestSuite = ({ contract, constructorArgs }) => this.addr2.expected = { mintCount: 20, - tokens: offsetted(0, 17, 1, 6, 7, 21, 13, 19, 10, 12, 11, 8, 20, 14, 15, 16, 3, 18, 22, 9), + tokens: offsetted(0, 1, 17, 6, 7, 21, 13, 19, 10, 12, 11, 8, 20, 14, 15, 16, 3, 18, 22, 9), }; this.addr3.expected = { @@ -62,38 +62,37 @@ const createTestSuite = ({ contract, constructorArgs }) => describe('successful transfers', async function () { beforeEach(async function () { const sender = this.addr2; - this.tokenIds = this.addr2.expected.tokens; + this.tokenIds = this.addr2.expected.tokens.slice(2); this.from = sender.address; this.to = transferToContract ? this.receiver : this.addr4; - this.approvedIds = [this.tokenIds[0], this.tokenIds[2], this.tokenIds[3]]; + this.approvedIds = [this.tokenIds[0], this.tokenIds[1]]; this.approvedIds.forEach(async (tokenId) => { await this.erc721aBatchTransferable.connect(sender).approve(this.to.address, tokenId); }); - const ownershipBefore = await this.erc721aBatchTransferable.getOwnershipAt(this.tokenIds[0]); + // Manually initialize some tokens of addr2 + await this.erc721aBatchTransferable.initializeOwnershipAt(8); + + const ownershipBefore = await this.erc721aBatchTransferable.getOwnershipAt(3); this.timestampBefore = parseInt(ownershipBefore.startTimestamp); this.timestampToMine = (await getBlockTimestamp()) + 12345; await mineBlockTimestamp(this.timestampToMine); this.timestampMined = await getBlockTimestamp(); - // Manually initialize some tokens of addr2 - await this.erc721aBatchTransferable.initializeOwnershipAt(3); - await this.erc721aBatchTransferable.initializeOwnershipAt(8); - // prettier-ignore this.transferTx = await this.erc721aBatchTransferable .connect(sender)[transferFn](this.from, this.to.address, this.tokenIds); + const ownershipAfter = await this.erc721aBatchTransferable.getOwnershipAt(3); + this.timestampAfter = parseInt(ownershipAfter.startTimestamp); + // Transfer part of uninitialized tokens this.tokensToTransferAlt = [25, 26, 27]; // prettier-ignore this.transferTxAlt = await this.erc721aBatchTransferable.connect(this.addr3)[transferFn]( this.addr3.address, this.addr5.address, this.tokensToTransferAlt ); - - const ownershipAfter = await this.erc721aBatchTransferable.getOwnershipAt(this.tokenIds[0]); - this.timestampAfter = parseInt(ownershipAfter.startTimestamp); }); it('emits Transfers event', async function () { @@ -106,9 +105,10 @@ const createTestSuite = ({ contract, constructorArgs }) => }); it('adjusts owners balances', async function () { - expect(await this.erc721aBatchTransferable.balanceOf(this.from)).to.be.equal(0); + const tokensNotTransferred = 2; + expect(await this.erc721aBatchTransferable.balanceOf(this.from)).to.be.equal(tokensNotTransferred); expect(await this.erc721aBatchTransferable.balanceOf(this.to.address)).to.be.equal( - this.addr2.expected.mintCount + this.addr2.expected.mintCount - tokensNotTransferred ); expect(await this.erc721aBatchTransferable.balanceOf(this.addr3.address)).to.be.equal( this.addr3.expected.tokens.length - this.tokensToTransferAlt.length @@ -194,6 +194,41 @@ const createTestSuite = ({ contract, constructorArgs }) => (await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[2]))[0] ).to.be.equal(transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.addr5.address); }); + + it('with first token transferred', async function () { + expect(await this.erc721aBatchTransferable.ownerOf(0)).to.be.equal(this.addr2.address); + expect(await this.erc721aBatchTransferable.ownerOf(1)).to.be.equal(this.addr2.address); + expect((await this.erc721aBatchTransferable.getOwnershipAt(0))[0]).to.be.equal(this.addr2.address); + expect((await this.erc721aBatchTransferable.getOwnershipAt(1))[0]).to.be.equal(ZERO_ADDRESS); + + // prettier-ignore + await this.erc721aBatchTransferable + .connect(this.addr2)[transferFn](this.addr2.address, this.to.address, [0]); + + expect(await this.erc721aBatchTransferable.ownerOf(0)).to.be.equal(this.to.address); + expect(await this.erc721aBatchTransferable.ownerOf(1)).to.be.equal(this.addr2.address); + expect((await this.erc721aBatchTransferable.getOwnershipAt(0))[0]).to.be.equal(this.to.address); + expect((await this.erc721aBatchTransferable.getOwnershipAt(1))[0]).to.be.equal(this.addr2.address); + }); + + it('with last token transferred', async function () { + await expect(this.erc721aBatchTransferable.ownerOf(this.numTotalTokens)).to.be.revertedWith( + 'OwnerQueryForNonexistentToken' + ); + + // prettier-ignore + await this.erc721aBatchTransferable + .connect(this.addr3)[transferFn]( + this.addr3.address, this.to.address, [offsetted(this.numTotalTokens - 1 + )]); + + expect(await this.erc721aBatchTransferable.ownerOf(offsetted(this.numTotalTokens - 1))).to.be.equal( + this.to.address + ); + await expect(this.erc721aBatchTransferable.ownerOf(this.numTotalTokens)).to.be.revertedWith( + 'OwnerQueryForNonexistentToken' + ); + }); }); }); }; From 5fea25dcb0577cd37990e78ce97fd08572cba759 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 20 Feb 2023 15:45:34 +0100 Subject: [PATCH 45/74] tests wip --- .../ERC721ABatchTransferable.test.js | 243 +++++++++++------- 1 file changed, 153 insertions(+), 90 deletions(-) diff --git a/test/extensions/ERC721ABatchTransferable.test.js b/test/extensions/ERC721ABatchTransferable.test.js index 29ec93f39..93eb5879c 100644 --- a/test/extensions/ERC721ABatchTransferable.test.js +++ b/test/extensions/ERC721ABatchTransferable.test.js @@ -32,7 +32,6 @@ const createTestSuite = ({ contract, constructorArgs }) => this.addr3 = addr3; this.addr4 = addr4; this.addr5 = addr5; - this.numTotalTokens = 30; this.addr1.expected = { mintCount: 3, @@ -49,6 +48,9 @@ const createTestSuite = ({ contract, constructorArgs }) => tokens: offsetted(23, 24, 25, 26, 27, 28, 29), }; + this.numTotalTokens = + this.addr1.expected.mintCount + this.addr2.expected.mintCount + this.addr3.expected.mintCount; + await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr2.address, 2); await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr1.address, 1); await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr2.address, 1); @@ -66,13 +68,15 @@ const createTestSuite = ({ contract, constructorArgs }) => this.from = sender.address; this.to = transferToContract ? this.receiver : this.addr4; this.approvedIds = [this.tokenIds[0], this.tokenIds[1]]; + this.initializedToken = 8; + this.uninitializedToken = 10; this.approvedIds.forEach(async (tokenId) => { await this.erc721aBatchTransferable.connect(sender).approve(this.to.address, tokenId); }); // Manually initialize some tokens of addr2 - await this.erc721aBatchTransferable.initializeOwnershipAt(8); + await this.erc721aBatchTransferable.initializeOwnershipAt(this.initializedToken); const ownershipBefore = await this.erc721aBatchTransferable.getOwnershipAt(3); this.timestampBefore = parseInt(ownershipBefore.startTimestamp); @@ -131,104 +135,163 @@ const createTestSuite = ({ contract, constructorArgs }) => expect(this.timestampToMine).to.be.eq(this.timestampMined); }); - describe('ownership correctly set', async function () { - it('with transfer of the given token IDs to the given address', async function () { - for (let i = 0; i < this.tokenIds.length; i++) { - const tokenId = this.tokenIds[i]; - expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.to.address); - } - - // Initialized tokens were updated - expect((await this.erc721aBatchTransferable.getOwnershipAt(3))[0]).to.be.equal(this.to.address); - - // Initialized tokens in a consecutive transfer are cleared - expect((await this.erc721aBatchTransferable.getOwnershipAt(8))[0]).to.be.equal( - transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.to.address - ); - - // Uninitialized tokens are left uninitialized - expect((await this.erc721aBatchTransferable.getOwnershipAt(7))[0]).to.be.equal( - transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.to.address - ); - - // Other tokens in between are left unchanged - for (let i = 0; i < this.addr1.expected.tokens.length; i++) { - const tokenId = this.addr1.expected.tokens[i]; - expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr1.address); - } - }); + it('with transfer of the given token IDs to the given address', async function () { + for (let i = 0; i < this.tokenIds.length; i++) { + const tokenId = this.tokenIds[i]; + expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.to.address); + } - it('with transfers of uninitialized token IDs to the given address', async function () { - const allTokensInitiallyOwned = this.addr3.expected.tokens; - allTokensInitiallyOwned.splice(2, this.tokensToTransferAlt.length); - - for (let i = 0; i < this.tokensToTransferAlt.length; i++) { - const tokenId = this.tokensToTransferAlt[i]; - expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr5.address); - } - - for (let i = 0; i < allTokensInitiallyOwned.length; i++) { - const tokenId = allTokensInitiallyOwned[i]; - expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr3.address); - } - - // Ownership of tokens was updated - expect( - (await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[0]))[0] - ).to.be.equal(this.addr5.address); - expect((await this.erc721aBatchTransferable.getOwnershipAt(allTokensInitiallyOwned[2]))[0]).to.be.equal( - this.addr3.address - ); - - // Uninitialized tokens are left uninitialized - expect( - (await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[0] - 1))[0] - ).to.be.equal(ZERO_ADDRESS); - expect((await this.erc721aBatchTransferable.getOwnershipAt(allTokensInitiallyOwned[3]))[0]).to.be.equal( - ZERO_ADDRESS - ); - expect( - (await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[1]))[0] - ).to.be.equal(transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.addr5.address); - expect( - (await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[2]))[0] - ).to.be.equal(transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.addr5.address); - }); + // Initialized tokens were updated + expect((await this.erc721aBatchTransferable.getOwnershipAt(3))[0]).to.be.equal(this.to.address); - it('with first token transferred', async function () { - expect(await this.erc721aBatchTransferable.ownerOf(0)).to.be.equal(this.addr2.address); - expect(await this.erc721aBatchTransferable.ownerOf(1)).to.be.equal(this.addr2.address); - expect((await this.erc721aBatchTransferable.getOwnershipAt(0))[0]).to.be.equal(this.addr2.address); - expect((await this.erc721aBatchTransferable.getOwnershipAt(1))[0]).to.be.equal(ZERO_ADDRESS); + // Initialized tokens in a consecutive transfer are cleared + expect((await this.erc721aBatchTransferable.getOwnershipAt(8))[0]).to.be.equal( + transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.to.address + ); - // prettier-ignore - await this.erc721aBatchTransferable - .connect(this.addr2)[transferFn](this.addr2.address, this.to.address, [0]); + // Uninitialized tokens are left uninitialized + expect((await this.erc721aBatchTransferable.getOwnershipAt(7))[0]).to.be.equal( + transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.to.address + ); - expect(await this.erc721aBatchTransferable.ownerOf(0)).to.be.equal(this.to.address); - expect(await this.erc721aBatchTransferable.ownerOf(1)).to.be.equal(this.addr2.address); - expect((await this.erc721aBatchTransferable.getOwnershipAt(0))[0]).to.be.equal(this.to.address); - expect((await this.erc721aBatchTransferable.getOwnershipAt(1))[0]).to.be.equal(this.addr2.address); - }); + // Other tokens in between are left unchanged + for (let i = 0; i < this.addr1.expected.tokens.length; i++) { + const tokenId = this.addr1.expected.tokens[i]; + expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr1.address); + } + }); + + it('with transfers of uninitialized token IDs to the given address', async function () { + const allTokensInitiallyOwned = this.addr3.expected.tokens; + allTokensInitiallyOwned.splice(2, this.tokensToTransferAlt.length); + + for (let i = 0; i < this.tokensToTransferAlt.length; i++) { + const tokenId = this.tokensToTransferAlt[i]; + expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr5.address); + } + + for (let i = 0; i < allTokensInitiallyOwned.length; i++) { + const tokenId = allTokensInitiallyOwned[i]; + expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr3.address); + } + + // Ownership of tokens was updated + expect((await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[0]))[0]).to.be.equal( + this.addr5.address + ); + expect((await this.erc721aBatchTransferable.getOwnershipAt(allTokensInitiallyOwned[2]))[0]).to.be.equal( + this.addr3.address + ); + + // Uninitialized tokens are left uninitialized + expect( + (await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[0] - 1))[0] + ).to.be.equal(ZERO_ADDRESS); + expect((await this.erc721aBatchTransferable.getOwnershipAt(allTokensInitiallyOwned[3]))[0]).to.be.equal( + ZERO_ADDRESS + ); + expect((await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[1]))[0]).to.be.equal( + transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.addr5.address + ); + expect((await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[2]))[0]).to.be.equal( + transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.addr5.address + ); + }); + }); + + describe('ownership correctly set', async function () { + beforeEach(async function () { + const sender = this.addr2; + this.from = sender.address; + this.to = transferToContract ? this.receiver : this.addr4; + this.initializedToken = 8; + this.uninitializedToken = 10; + + // Manually initialize some tokens of addr2 + await this.erc721aBatchTransferable.initializeOwnershipAt(this.initializedToken); + }); - it('with last token transferred', async function () { - await expect(this.erc721aBatchTransferable.ownerOf(this.numTotalTokens)).to.be.revertedWith( - 'OwnerQueryForNonexistentToken' - ); + it('with first token transferred', async function () { + expect(await this.erc721aBatchTransferable.ownerOf(0)).to.be.equal(this.from); + expect(await this.erc721aBatchTransferable.ownerOf(1)).to.be.equal(this.from); + expect((await this.erc721aBatchTransferable.getOwnershipAt(0))[0]).to.be.equal(this.from); + expect((await this.erc721aBatchTransferable.getOwnershipAt(1))[0]).to.be.equal(ZERO_ADDRESS); - // prettier-ignore - await this.erc721aBatchTransferable + // prettier-ignore + await this.erc721aBatchTransferable + .connect(this.addr2)[transferFn](this.from, this.to.address, [0]); + + expect(await this.erc721aBatchTransferable.ownerOf(0)).to.be.equal(this.to.address); + expect(await this.erc721aBatchTransferable.ownerOf(1)).to.be.equal(this.from); + expect((await this.erc721aBatchTransferable.getOwnershipAt(0))[0]).to.be.equal(this.to.address); + expect((await this.erc721aBatchTransferable.getOwnershipAt(1))[0]).to.be.equal(this.from); + }); + + it('with initialized token transferred', async function () { + expect(await this.erc721aBatchTransferable.ownerOf(this.initializedToken)).to.be.equal(this.from); + expect(await this.erc721aBatchTransferable.ownerOf(this.initializedToken + 1)).to.be.equal(this.from); + expect((await this.erc721aBatchTransferable.getOwnershipAt(this.initializedToken))[0]).to.be.equal( + this.from + ); + expect((await this.erc721aBatchTransferable.getOwnershipAt(this.initializedToken + 1))[0]).to.be.equal( + ZERO_ADDRESS + ); + + // prettier-ignore + await this.erc721aBatchTransferable + .connect(this.addr2)[transferFn](this.from, this.to.address, [this.initializedToken]); + + expect(await this.erc721aBatchTransferable.ownerOf(this.initializedToken)).to.be.equal(this.to.address); + expect(await this.erc721aBatchTransferable.ownerOf(this.initializedToken + 1)).to.be.equal(this.from); + expect((await this.erc721aBatchTransferable.getOwnershipAt(this.initializedToken))[0]).to.be.equal( + this.to.address + ); + expect((await this.erc721aBatchTransferable.getOwnershipAt(this.initializedToken + 1))[0]).to.be.equal( + this.from + ); + }); + + it('with uninitialized token transferred', async function () { + expect(await this.erc721aBatchTransferable.ownerOf(this.uninitializedToken)).to.be.equal(this.from); + expect(await this.erc721aBatchTransferable.ownerOf(this.uninitializedToken + 1)).to.be.equal(this.from); + expect((await this.erc721aBatchTransferable.getOwnershipAt(this.uninitializedToken))[0]).to.be.equal( + ZERO_ADDRESS + ); + expect((await this.erc721aBatchTransferable.getOwnershipAt(this.uninitializedToken + 1))[0]).to.be.equal( + ZERO_ADDRESS + ); + + // prettier-ignore + await this.erc721aBatchTransferable + .connect(this.addr2)[transferFn](this.from, this.to.address, [this.uninitializedToken]); + + expect(await this.erc721aBatchTransferable.ownerOf(this.uninitializedToken)).to.be.equal(this.to.address); + expect(await this.erc721aBatchTransferable.ownerOf(this.uninitializedToken + 1)).to.be.equal(this.from); + expect((await this.erc721aBatchTransferable.getOwnershipAt(this.uninitializedToken))[0]).to.be.equal( + this.to.address + ); + expect((await this.erc721aBatchTransferable.getOwnershipAt(this.uninitializedToken + 1))[0]).to.be.equal( + this.from + ); + }); + + it('with last token transferred', async function () { + await expect(this.erc721aBatchTransferable.ownerOf(this.numTotalTokens)).to.be.revertedWith( + 'OwnerQueryForNonexistentToken' + ); + + // prettier-ignore + await this.erc721aBatchTransferable .connect(this.addr3)[transferFn]( this.addr3.address, this.to.address, [offsetted(this.numTotalTokens - 1 )]); - expect(await this.erc721aBatchTransferable.ownerOf(offsetted(this.numTotalTokens - 1))).to.be.equal( - this.to.address - ); - await expect(this.erc721aBatchTransferable.ownerOf(this.numTotalTokens)).to.be.revertedWith( - 'OwnerQueryForNonexistentToken' - ); - }); + expect(await this.erc721aBatchTransferable.ownerOf(offsetted(this.numTotalTokens - 1))).to.be.equal( + this.to.address + ); + await expect(this.erc721aBatchTransferable.ownerOf(this.numTotalTokens)).to.be.revertedWith( + 'OwnerQueryForNonexistentToken' + ); }); }); }; From ea7a3c265d9cbd09ef8ec11f5af966ce937bac20 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 20 Feb 2023 18:48:51 +0100 Subject: [PATCH 46/74] comments --- contracts/ERC721A.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 003ea2de5..c3fffd8bb 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -725,11 +725,11 @@ contract ERC721A is IERC721A { startTokenId = tokenIds[i]; totalTokensLeft = totalTokens - i; - // Update `prevOwnershipPacked` and check ownership of `startTokenId`. + // Updates `prevOwnershipPacked` and check ownership of `startTokenId`. prevOwnershipPacked = _packedOwnershipOf(startTokenId); if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); - // Update startTokenId: + // Updates startTokenId: // - `address` to the next owner. // - `startTimestamp` to the timestamp of transfering. // - `burned` to `false`. @@ -768,10 +768,10 @@ contract ERC721A is IERC721A { // If there is extra data to preserve if (nextOwnershipPacked >> _BITPOS_EXTRA_DATA != 0) { - // Update `prevOwnershipPacked` with last initialized `nextOwnershipPacked`. + // Updates `prevOwnershipPacked` with last initialized `nextOwnershipPacked`. prevOwnershipPacked = nextOwnershipPacked; - // Update `nextTokenId` + // Updates `nextTokenId` // - `address` to the next owner. // - `startTimestamp` to the timestamp of transfering. // - `burned` to `false`. @@ -1408,7 +1408,7 @@ contract ERC721A is IERC721A { _packedAddressData[from] += (totalTokens << _BITPOS_NUMBER_BURNED) - totalTokens; for (uint256 i; i < totalTokens; ) { - // Except during first loop, where this logic has already been executed. + // Skip during first loop, where this logic has already been executed. if (i != 0) { startTokenId = tokenIds[i]; totalTokensLeft = totalTokens - i; @@ -1463,7 +1463,7 @@ contract ERC721A is IERC721A { // Update `prevOwnershipPacked` with last initialized `nextOwnershipPacked`. prevOwnershipPacked = nextOwnershipPacked; - // Update `nextTokenId` + // Updates `nextTokenId` // - `address` to the last owner. // - `startTimestamp` to the timestamp of burning. // - `burned` to `true`. From 024640232daed8ba9aba8c4848f9a2b18ee4ee0f Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 20 Feb 2023 18:51:27 +0100 Subject: [PATCH 47/74] added extraData logic to batch mocks --- contracts/mocks/ERC721ABatchBurnableMock.sol | 12 ++++++++++++ contracts/mocks/ERC721ABatchTransferableMock.sol | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/contracts/mocks/ERC721ABatchBurnableMock.sol b/contracts/mocks/ERC721ABatchBurnableMock.sol index aa6dfc287..0d862d505 100644 --- a/contracts/mocks/ERC721ABatchBurnableMock.sol +++ b/contracts/mocks/ERC721ABatchBurnableMock.sol @@ -37,6 +37,18 @@ contract ERC721ABatchBurnableMock is ERC721ABatchBurnable { _initializeOwnershipAt(index); } + function _extraData( + address, + address, + uint24 previousExtraData + ) internal view virtual override returns (uint24) { + return previousExtraData; + } + + function setExtraDataAt(uint256 index, uint24 extraData) public { + _setExtraDataAt(index, extraData); + } + function batchBurnUnoptimized(uint256[] memory tokenIds) public { unchecked { uint256 tokenId; diff --git a/contracts/mocks/ERC721ABatchTransferableMock.sol b/contracts/mocks/ERC721ABatchTransferableMock.sol index 0442e25fc..d0aaea814 100644 --- a/contracts/mocks/ERC721ABatchTransferableMock.sol +++ b/contracts/mocks/ERC721ABatchTransferableMock.sol @@ -41,6 +41,18 @@ contract ERC721ABatchTransferableMock is ERC721ABatchTransferable { _initializeOwnershipAt(index); } + function _extraData( + address, + address, + uint24 previousExtraData + ) internal view virtual override returns (uint24) { + return previousExtraData; + } + + function setExtraDataAt(uint256 index, uint24 extraData) public { + _setExtraDataAt(index, extraData); + } + function batchTransferFromUnoptimized( address from, address to, From 3949159f5feda6f7cdc133c39eda3e3770ea486b Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 20 Feb 2023 18:51:41 +0100 Subject: [PATCH 48/74] updated batch tests --- test/extensions/ERC721ABatchBurnable.test.js | 139 ++++++++++++++++++ .../ERC721ABatchTransferable.test.js | 116 ++++++++++++--- 2 files changed, 235 insertions(+), 20 deletions(-) diff --git a/test/extensions/ERC721ABatchBurnable.test.js b/test/extensions/ERC721ABatchBurnable.test.js index 02308c7d0..400e63ce1 100644 --- a/test/extensions/ERC721ABatchBurnable.test.js +++ b/test/extensions/ERC721ABatchBurnable.test.js @@ -30,9 +30,15 @@ const createTestSuite = ({ contract, constructorArgs }) => this.notBurnedTokenId2 = 5; this.notBurnedTokenId3 = 6; this.notBurnedTokenId4 = 8; + this.initializedToken = 12; + this.uninitializedToken = 13; + await this.erc721aBatchBurnable['safeMint(address,uint256)'](this.addr1.address, this.numTestTokens); await this.erc721aBatchBurnable.connect(this.addr1).batchBurn(this.burnedTokenIds1); await this.erc721aBatchBurnable.connect(this.addr1).batchBurn(this.burnedTokenIds2); + + // Manually initialize `this.initializedToken` + await this.erc721aBatchBurnable.initializeOwnershipAt(this.initializedToken); }); context('totalSupply()', function () { @@ -60,6 +66,7 @@ const createTestSuite = ({ contract, constructorArgs }) => it('changes totalBurned', async function () { const totalBurnedBefore = (await this.erc721aBatchBurnable.totalBurned()).toNumber(); + expect(totalBurnedBefore).to.equal(this.totalBurned); await this.erc721aBatchBurnable .connect(this.addr1) @@ -138,6 +145,30 @@ const createTestSuite = ({ contract, constructorArgs }) => await expect(query).to.be.revertedWith('OwnerQueryForNonexistentToken'); }); + it('cannot burn tokens with different owners', async function () { + const tokenIdsToBurn = [this.notBurnedTokenId1, this.notBurnedTokenId2, this.notBurnedTokenId3]; + + await this.erc721aBatchBurnable.connect(this.addr1).setApprovalForAll(this.spender.address, true); + await this.erc721aBatchBurnable + .connect(this.addr1) + .transferFrom(this.addr1.address, this.spender.address, this.notBurnedTokenId2); + + await expect( + this.erc721aBatchBurnable.connect(this.spender).batchBurn(tokenIdsToBurn.slice(0, 2)) + ).to.be.revertedWith('TransferFromIncorrectOwner'); + // Expect also consecutive ids to revert + await expect( + this.erc721aBatchBurnable.connect(this.spender).batchBurn(tokenIdsToBurn.slice(1)) + ).to.be.revertedWith('TransferFromIncorrectOwner'); + + await expect( + this.erc721aBatchBurnable.connect(this.spender).batchBurn([this.notBurnedTokenId1]) + ).to.not.be.reverted; + await expect( + this.erc721aBatchBurnable.connect(this.spender).batchBurn([this.notBurnedTokenId2]) + ).to.not.be.reverted; + }); + it('does not affect _totalMinted', async function () { const tokenIdsToBurn = [this.notBurnedTokenId1, this.notBurnedTokenId2]; const totalMintedBefore = await this.erc721aBatchBurnable.totalMinted(); @@ -169,6 +200,66 @@ const createTestSuite = ({ contract, constructorArgs }) => }); describe('ownerships correctly set', async function () { + it('with tokens burned', async function () { + await this.erc721aBatchBurnable.connect(this.addr1).batchBurn([this.notBurnedTokenId1]); + + for (let i = 0; i < this.numTestTokens; ++i) { + const initializedTokens = [0, 2, 5, 7, 8, 9, 11, 12, this.notBurnedTokenId1]; + + expect((await this.erc721aBatchBurnable.getOwnershipAt(i))[0]).to.be.equal( + initializedTokens.includes(i) ? this.addr1.address : ZERO_ADDRESS + ); + } + }); + + it('with tokens burned and cleared', async function () { + const initializedToken = 15; + + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal(ZERO_ADDRESS); + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(ZERO_ADDRESS); + + // Initialize token + await this.erc721aBatchBurnable.initializeOwnershipAt(initializedToken); + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal(ZERO_ADDRESS); + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(this.addr1.address); + + // Burn tokens + await this.erc721aBatchBurnable.connect(this.addr1).batchBurn([initializedToken - 1, initializedToken]); + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( + this.addr1.address + ); + + // Initialized tokens in a consecutive burn are cleared + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(ZERO_ADDRESS); + }); + + it('with tokens burned and updated', async function () { + const initializedToken = 15; + const extraData = 123; + + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal(ZERO_ADDRESS); + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(ZERO_ADDRESS); + + // Initialize token + await this.erc721aBatchBurnable.initializeOwnershipAt(initializedToken); + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal(ZERO_ADDRESS); + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(this.addr1.address); + + // Set extra data + await this.erc721aBatchBurnable.setExtraDataAt(initializedToken, extraData); + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[3]).to.be.equal(extraData); + + // Burn tokens + await this.erc721aBatchBurnable.connect(this.addr1).batchBurn([initializedToken - 1, initializedToken]); + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( + this.addr1.address + ); + + // Initialized tokens in a consecutive burn are updated when nextData is not 0 + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(this.addr1.address); + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[3]).to.be.equal(extraData); + }); + it('with token before previously burnt token transferred and burned', async function () { await this.erc721aBatchBurnable .connect(this.addr1) @@ -224,6 +315,54 @@ const createTestSuite = ({ contract, constructorArgs }) => 'OwnerQueryForNonexistentToken' ); }); + + it('with initialized token transferred', async function () { + expect(await this.erc721aBatchBurnable.ownerOf(this.initializedToken)).to.be.equal(this.addr1.address); + expect(await this.erc721aBatchBurnable.ownerOf(this.initializedToken + 1)).to.be.equal(this.addr1.address); + expect((await this.erc721aBatchBurnable.getOwnershipAt(this.initializedToken))[0]).to.be.equal( + this.addr1.address + ); + expect((await this.erc721aBatchBurnable.getOwnershipAt(this.initializedToken + 1))[0]).to.be.equal( + ZERO_ADDRESS + ); + + await this.erc721aBatchBurnable.connect(this.addr1).batchBurn([this.initializedToken]); + + await expect(this.erc721aBatchBurnable.ownerOf(this.initializedToken)).to.be.revertedWith( + 'OwnerQueryForNonexistentToken' + ); + expect(await this.erc721aBatchBurnable.ownerOf(this.initializedToken + 1)).to.be.equal(this.addr1.address); + expect((await this.erc721aBatchBurnable.getOwnershipAt(this.initializedToken))[0]).to.be.equal( + this.addr1.address + ); + expect((await this.erc721aBatchBurnable.getOwnershipAt(this.initializedToken + 1))[0]).to.be.equal( + this.addr1.address + ); + }); + + it('with uninitialized token transferred', async function () { + expect(await this.erc721aBatchBurnable.ownerOf(this.uninitializedToken)).to.be.equal(this.addr1.address); + expect(await this.erc721aBatchBurnable.ownerOf(this.uninitializedToken + 1)).to.be.equal(this.addr1.address); + expect((await this.erc721aBatchBurnable.getOwnershipAt(this.uninitializedToken))[0]).to.be.equal( + ZERO_ADDRESS + ); + expect((await this.erc721aBatchBurnable.getOwnershipAt(this.uninitializedToken + 1))[0]).to.be.equal( + ZERO_ADDRESS + ); + + await this.erc721aBatchBurnable.connect(this.addr1).batchBurn([this.uninitializedToken]); + + await expect(this.erc721aBatchBurnable.ownerOf(this.uninitializedToken)).to.be.revertedWith( + 'OwnerQueryForNonexistentToken' + ); + expect(await this.erc721aBatchBurnable.ownerOf(this.uninitializedToken + 1)).to.be.equal(this.addr1.address); + expect((await this.erc721aBatchBurnable.getOwnershipAt(this.uninitializedToken))[0]).to.be.equal( + this.addr1.address + ); + expect((await this.erc721aBatchBurnable.getOwnershipAt(this.uninitializedToken + 1))[0]).to.be.equal( + this.addr1.address + ); + }); }); }); }; diff --git a/test/extensions/ERC721ABatchTransferable.test.js b/test/extensions/ERC721ABatchTransferable.test.js index 93eb5879c..ac67a1542 100644 --- a/test/extensions/ERC721ABatchTransferable.test.js +++ b/test/extensions/ERC721ABatchTransferable.test.js @@ -75,7 +75,7 @@ const createTestSuite = ({ contract, constructorArgs }) => await this.erc721aBatchTransferable.connect(sender).approve(this.to.address, tokenId); }); - // Manually initialize some tokens of addr2 + // Manually initialize `this.initializedToken` await this.erc721aBatchTransferable.initializeOwnershipAt(this.initializedToken); const ownershipBefore = await this.erc721aBatchTransferable.getOwnershipAt(3); @@ -211,6 +211,82 @@ const createTestSuite = ({ contract, constructorArgs }) => await this.erc721aBatchTransferable.initializeOwnershipAt(this.initializedToken); }); + it('with tokens transferred and cleared', async function () { + const initializedToken = 15; + + expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( + ZERO_ADDRESS + ); + expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken))[0]).to.be.equal( + ZERO_ADDRESS + ); + + // Initialize token + await this.erc721aBatchTransferable.initializeOwnershipAt(initializedToken); + expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( + ZERO_ADDRESS + ); + expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken))[0]).to.be.equal( + this.addr2.address + ); + + // Transfer tokens + // prettier-ignore + await this.erc721aBatchTransferable + .connect(this.addr2)[transferFn]( + this.from, this.to.address, [initializedToken - 1, initializedToken] + ); + expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( + this.to.address + ); + + // Initialized tokens in a consecutive transfer are cleared + expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken))[0]).to.be.equal( + transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.to.address + ); + }); + + it('with tokens transferred and updated', async function () { + const initializedToken = 15; + const extraData = 123; + + expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( + ZERO_ADDRESS + ); + expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken))[0]).to.be.equal( + ZERO_ADDRESS + ); + + // Initialize token + await this.erc721aBatchTransferable.initializeOwnershipAt(initializedToken); + expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( + ZERO_ADDRESS + ); + expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken))[0]).to.be.equal( + this.addr2.address + ); + + // Set extra data + await this.erc721aBatchTransferable.setExtraDataAt(initializedToken, extraData); + expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken))[3]).to.be.equal(extraData); + + // Transfer tokens + // prettier-ignore + await this.erc721aBatchTransferable + .connect(this.addr2)[transferFn]( + this.from, this.to.address, [initializedToken - 1, initializedToken] + ); + expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( + this.to.address + ); + + // Initialized tokens in a consecutive transfer are updated when nextData is not 0 + expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken))[0]).to.be.equal( + this.to.address + ); + expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken))[3]).to.be.equal(extraData); + }); + it('with first token transferred', async function () { expect(await this.erc721aBatchTransferable.ownerOf(0)).to.be.equal(this.from); expect(await this.erc721aBatchTransferable.ownerOf(1)).to.be.equal(this.from); @@ -227,6 +303,25 @@ const createTestSuite = ({ contract, constructorArgs }) => expect((await this.erc721aBatchTransferable.getOwnershipAt(1))[0]).to.be.equal(this.from); }); + it('with last token transferred', async function () { + await expect(this.erc721aBatchTransferable.ownerOf(this.numTotalTokens)).to.be.revertedWith( + 'OwnerQueryForNonexistentToken' + ); + + // prettier-ignore + await this.erc721aBatchTransferable + .connect(this.addr3)[transferFn]( + this.addr3.address, this.to.address, [offsetted(this.numTotalTokens - 1 + )]); + + expect(await this.erc721aBatchTransferable.ownerOf(offsetted(this.numTotalTokens - 1))).to.be.equal( + this.to.address + ); + await expect(this.erc721aBatchTransferable.ownerOf(this.numTotalTokens)).to.be.revertedWith( + 'OwnerQueryForNonexistentToken' + ); + }); + it('with initialized token transferred', async function () { expect(await this.erc721aBatchTransferable.ownerOf(this.initializedToken)).to.be.equal(this.from); expect(await this.erc721aBatchTransferable.ownerOf(this.initializedToken + 1)).to.be.equal(this.from); @@ -274,25 +369,6 @@ const createTestSuite = ({ contract, constructorArgs }) => this.from ); }); - - it('with last token transferred', async function () { - await expect(this.erc721aBatchTransferable.ownerOf(this.numTotalTokens)).to.be.revertedWith( - 'OwnerQueryForNonexistentToken' - ); - - // prettier-ignore - await this.erc721aBatchTransferable - .connect(this.addr3)[transferFn]( - this.addr3.address, this.to.address, [offsetted(this.numTotalTokens - 1 - )]); - - expect(await this.erc721aBatchTransferable.ownerOf(offsetted(this.numTotalTokens - 1))).to.be.equal( - this.to.address - ); - await expect(this.erc721aBatchTransferable.ownerOf(this.numTotalTokens)).to.be.revertedWith( - 'OwnerQueryForNonexistentToken' - ); - }); }); }; From 8c3f49751cc448fc2d47545176bf7630606773df Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 20 Feb 2023 19:07:55 +0100 Subject: [PATCH 49/74] refactored ERC721A to use _updateTokenId --- contracts/ERC721A.sol | 60 +++++++++++++++---------------------------- 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index c3fffd8bb..614954db9 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -587,18 +587,8 @@ contract ERC721A is IERC721A { _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked) ); - // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . - if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { - uint256 nextTokenId = tokenId + 1; - // If the next slot's address is zero and not burned (i.e. packed value is zero). - if (_packedOwnerships[nextTokenId] == 0) { - // If the next slot is within bounds. - if (nextTokenId != _currentIndex) { - // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. - _packedOwnerships[nextTokenId] = prevOwnershipPacked; - } - } - } + // Updates next slot if necessary. + _updateTokenId(tokenId + 1, prevOwnershipPacked, prevOwnershipPacked); } // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean. @@ -745,7 +735,7 @@ contract ERC721A is IERC721A { // If `nextTokenId` is not consecutive, update `nextTokenId` and break from the loop. if (tokenIds[i + quantity] != nextTokenId) { - _updateNextTokenId( + _updateTokenId( nextTokenId, quantity == 1 ? prevOwnershipPacked : nextOwnershipPacked, prevOwnershipPacked @@ -797,9 +787,9 @@ contract ERC721A is IERC721A { // Update the tokenId subsequent to the last element in `tokenIds` if (quantity == 1) { - _updateNextTokenId(startTokenId + 1, prevOwnershipPacked, prevOwnershipPacked); + _updateTokenId(startTokenId + 1, prevOwnershipPacked, prevOwnershipPacked); } else { - _updateNextTokenId(nextTokenId + 1, nextOwnershipPacked, prevOwnershipPacked); + _updateTokenId(nextTokenId + 1, nextOwnershipPacked, prevOwnershipPacked); } } @@ -991,25 +981,25 @@ contract ERC721A is IERC721A { } /** - * @dev Private function to handle updating ownership of `nextTokenId`. Used in `_batchTransferFrom`. + * @dev Private function to handle updating ownership of `tokenId`. Used in `_batchTransferFrom`. * - * `nextTokenId` - Token ID to initialize. - * `lastOwnershipPacked` - Slot of `nextTokenId - 1` - * `prevOwnershipPacked` - Last initialized slot before `nextTokenId` + * `tokenId` - Token ID to initialize. + * `lastOwnershipPacked` - Slot of `tokenId - 1` + * `prevOwnershipPacked` - Last initialized slot before `tokenId` */ - function _updateNextTokenId( - uint256 nextTokenId, + function _updateTokenId( + uint256 tokenId, uint256 lastOwnershipPacked, uint256 prevOwnershipPacked ) private { // If the next slot may not have been initialized (i.e. `nextInitialized == false`). if (lastOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { // If the next slot's address is zero and not burned (i.e. packed value is zero). - if (_packedOwnerships[nextTokenId] == 0) { + if (_packedOwnerships[tokenId] == 0) { // If the next slot is within bounds. - if (nextTokenId != _currentIndex) { - // Initialize the next slot to maintain correctness for `ownerOf(nextTokenId)`. - _packedOwnerships[nextTokenId] = prevOwnershipPacked; + if (tokenId != _currentIndex) { + // Initialize the next slot to maintain correctness for `ownerOf(tokenId)`. + _packedOwnerships[tokenId] = prevOwnershipPacked; } } } @@ -1331,18 +1321,8 @@ contract ERC721A is IERC721A { (_BITMASK_BURNED | _BITMASK_NEXT_INITIALIZED) | _nextExtraData(from, address(0), prevOwnershipPacked) ); - // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . - if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { - uint256 nextTokenId = tokenId + 1; - // If the next slot's address is zero and not burned (i.e. packed value is zero). - if (_packedOwnerships[nextTokenId] == 0) { - // If the next slot is within bounds. - if (nextTokenId != _currentIndex) { - // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. - _packedOwnerships[nextTokenId] = prevOwnershipPacked; - } - } - } + // Updates next slot if necessary. + _updateTokenId(tokenId + 1, prevOwnershipPacked, prevOwnershipPacked); } emit Transfer(from, address(0), tokenId); @@ -1437,7 +1417,7 @@ contract ERC721A is IERC721A { // If `nextTokenId` is not consecutive, update `nextTokenId` and break from the loop. if (tokenIds[i + quantity] != nextTokenId) { - _updateNextTokenId( + _updateTokenId( nextTokenId, quantity == 1 ? prevOwnershipPacked : nextOwnershipPacked, prevOwnershipPacked @@ -1488,9 +1468,9 @@ contract ERC721A is IERC721A { // Update the tokenId subsequent to the last element in `tokenIds` if (quantity == 1) { - _updateNextTokenId(startTokenId + 1, prevOwnershipPacked, prevOwnershipPacked); + _updateTokenId(startTokenId + 1, prevOwnershipPacked, prevOwnershipPacked); } else { - _updateNextTokenId(nextTokenId + 1, nextOwnershipPacked, prevOwnershipPacked); + _updateTokenId(nextTokenId + 1, nextOwnershipPacked, prevOwnershipPacked); } // Overflow not possible, as _burnCounter cannot be exceed _currentIndex times. From 735c366fa282a989a10ad59408cf28039af65fba Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 20 Feb 2023 19:44:25 +0100 Subject: [PATCH 50/74] wip --- test/extensions/ERC721ABatchBurnable.test.js | 5 +++-- test/extensions/ERC721ABatchTransferable.test.js | 9 ++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/extensions/ERC721ABatchBurnable.test.js b/test/extensions/ERC721ABatchBurnable.test.js index 400e63ce1..170c7ebe0 100644 --- a/test/extensions/ERC721ABatchBurnable.test.js +++ b/test/extensions/ERC721ABatchBurnable.test.js @@ -34,8 +34,9 @@ const createTestSuite = ({ contract, constructorArgs }) => this.uninitializedToken = 13; await this.erc721aBatchBurnable['safeMint(address,uint256)'](this.addr1.address, this.numTestTokens); - await this.erc721aBatchBurnable.connect(this.addr1).batchBurn(this.burnedTokenIds1); - await this.erc721aBatchBurnable.connect(this.addr1).batchBurn(this.burnedTokenIds2); + await this.erc721aBatchBurnable + .connect(this.addr1) + .batchBurn([...this.burnedTokenIds2, ...this.burnedTokenIds1]); // Manually initialize `this.initializedToken` await this.erc721aBatchBurnable.initializeOwnershipAt(this.initializedToken); diff --git a/test/extensions/ERC721ABatchTransferable.test.js b/test/extensions/ERC721ABatchTransferable.test.js index ac67a1542..38ce7c38c 100644 --- a/test/extensions/ERC721ABatchTransferable.test.js +++ b/test/extensions/ERC721ABatchTransferable.test.js @@ -64,10 +64,10 @@ const createTestSuite = ({ contract, constructorArgs }) => describe('successful transfers', async function () { beforeEach(async function () { const sender = this.addr2; - this.tokenIds = this.addr2.expected.tokens.slice(2); + this.tokenIds = this.addr2.expected.tokens; this.from = sender.address; this.to = transferToContract ? this.receiver : this.addr4; - this.approvedIds = [this.tokenIds[0], this.tokenIds[1]]; + this.approvedIds = [this.tokenIds[2], this.tokenIds[3]]; this.initializedToken = 8; this.uninitializedToken = 10; @@ -109,10 +109,9 @@ const createTestSuite = ({ contract, constructorArgs }) => }); it('adjusts owners balances', async function () { - const tokensNotTransferred = 2; - expect(await this.erc721aBatchTransferable.balanceOf(this.from)).to.be.equal(tokensNotTransferred); + expect(await this.erc721aBatchTransferable.balanceOf(this.from)).to.be.equal(0); expect(await this.erc721aBatchTransferable.balanceOf(this.to.address)).to.be.equal( - this.addr2.expected.mintCount - tokensNotTransferred + this.addr2.expected.mintCount ); expect(await this.erc721aBatchTransferable.balanceOf(this.addr3.address)).to.be.equal( this.addr3.expected.tokens.length - this.tokensToTransferAlt.length From a22c2f54dbd8ee60296d9e9c4b98b50a7a454bc9 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Tue, 21 Feb 2023 14:28:12 +0100 Subject: [PATCH 51/74] comment --- contracts/ERC721A.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 614954db9..b8bd807b8 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -872,7 +872,7 @@ contract ERC721A is IERC721A { * @dev Hook that is called before a set of serially-ordered token IDs * are about to be transferred. This includes minting. * And also called before burning one token. - * But not called on batch transfers. + * But not called on batch transfers and burns (see `_beforeTokenBatchTransfers`). * * `startTokenId` - the first token ID to be transferred. * `quantity` - the amount to be transferred. @@ -896,7 +896,7 @@ contract ERC721A is IERC721A { * @dev Hook that is called after a set of serially-ordered token IDs * have been transferred. This includes minting. * And also called after one token has been burned. - * But not called on batch transfers. + * But not called on batch transfers and burns (see `_afterTokenBatchTransfers`). * * `startTokenId` - the first token ID to be transferred. * `quantity` - the amount to be transferred. From 4c11d746431e89bac2c3e6eab642c080aa8d63b7 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Wed, 22 Feb 2023 11:18:05 +0800 Subject: [PATCH 52/74] Add ERC721ABatchBurnableMock (#450) --- contracts/ERC721A.sol | 110 +++++++++++++++++++ contracts/IERC721A.sol | 5 + contracts/mocks/ERC721ABatchBurnableMock.sol | 40 +++++++ 3 files changed, 155 insertions(+) create mode 100644 contracts/mocks/ERC721ABatchBurnableMock.sol diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index c0230ef45..f86e0b00f 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1030,6 +1030,107 @@ contract ERC721A is IERC721A { } } + /** + * @dev Destroys `tokenIds`. + * The approval is cleared when the token is burned. + * + * Requirements: + * + * - `tokenIds` must exist. + * - `tokenIds` must be strictly ascending. + * - `burner` must be the owner or approved to burn each of the token. + * + * Emits a {Transfer} event for each token burned. + */ + function _batchBurn(address burner, uint256[] memory tokenIds, bool approvalCheck) internal virtual { + // We can use unchecked as the length of `tokenIds` is bounded + // to a small number by the max block gas limit. + unchecked { + // The next `tokenId` to be minted (i.e. `_nextTokenId()`). + uint256 stop = _currentIndex; + + uint256 n = tokenIds.length; + + // For checking if the `tokenIds` are strictly ascending. + uint256 prevTokenId; + + for (uint256 i; i != n; ) { + uint256 tokenId = tokenIds[i]; + + // Revert `tokenId` is out of bounds. + if (_or(tokenId < _startTokenId(), stop <= tokenId)) revert OwnerQueryForNonexistentToken(); + + // Revert if `tokenIds` is not strictly ascending. + if (i != 0) + if (tokenId <= prevTokenId) revert TokenIdsNotStrictlyAscending(); + + // The initialized packed ownership slot's value. + uint256 prevOwnershipPacked; + // Scan backwards for an initialized packed ownership slot. + // ERC721A's invariant guarantees that there will always be an initialized slot as long as + // the start of the backwards scan falls within `[_startTokenId() .. _nextTokenId())`. + for (uint256 j = tokenId; (prevOwnershipPacked = _packedOwnerships[j]) == 0; ) --j; + + // If the initialized slot is burned, revert. + if (prevOwnershipPacked & _BITMASK_BURNED != 0) revert OwnerQueryForNonexistentToken(); + + // Unpack the `tokenOwner` from bits [0..159] of `prevOwnershipPacked`. + address tokenOwner = address(uint160(prevOwnershipPacked)); + + // Check if the burner is either the owner or an approved operator for all the + bool mayBurn = !approvalCheck || tokenOwner == burner || isApprovedForAll(tokenOwner, burner); + + uint256 offset; + uint256 currTokenId = tokenId; + do { + // Revert if the burner is not authorized to burn the token. + if (!mayBurn) + if (getApproved(currTokenId) != burner) revert TransferCallerNotOwnerNorApproved(); + // Call the hook. + _beforeTokenTransfers(tokenOwner, address(0), currTokenId, 1); + // Emit the `Transfer` event for burn. + emit Transfer(tokenOwner, address(0), currTokenId); + // Call the hook. + _afterTokenTransfers(tokenOwner, address(0), currTokenId, 1); + // Increment `offset` and update `currTokenId`. + currTokenId = tokenId + (++offset); + } while ( + // Neither out of bounds, nor at the end of `tokenIds`. + !_or(currTokenId == stop, i + offset == n) && + // Token ID is sequential. + tokenIds[i + offset] == currTokenId && + // The packed ownership slot is not initialized. + _packedOwnerships[currTokenId] == 0 + ); + + // Update the packed ownership for `tokenId` in ERC721A's storage. + _packedOwnerships[tokenId] = + _BITMASK_BURNED | + (block.timestamp << _BITPOS_START_TIMESTAMP) | + uint256(uint160(tokenOwner)); + + // If the slot after the mini batch is neither out of bounds, nor initialized. + if (currTokenId != stop) + if (_packedOwnerships[currTokenId] == 0) + _packedOwnerships[currTokenId] = prevOwnershipPacked; + + // Update the address data in ERC721A's storage. + // + // Note that this update has to be in the loop as tokens + // can be burned by an operator that is not the token owner. + _packedAddressData[tokenOwner] += (offset << _BITPOS_NUMBER_BURNED) - offset; + + // Advance `i` by `offset`, the number of tokens burned in the mini batch. + i += offset; + + // Set the `prevTokenId` for checking that the `tokenIds` is strictly ascending. + prevTokenId = currTokenId - 1; + } + // Increase the `_burnCounter` in ERC721A's storage. + _burnCounter += n; + } + } + // ============================================================= // EXTRA DATA OPERATIONS // ============================================================= @@ -1146,4 +1247,13 @@ contract ERC721A is IERC721A { revert(0x00, 0x04) } } + + /** + * @dev Branchless or. + */ + function _or(bool a, bool b) private pure returns (bool c) { + assembly { + c := or(a, b) + } + } } diff --git a/contracts/IERC721A.sol b/contracts/IERC721A.sol index 4bd87db45..a1aef23b3 100644 --- a/contracts/IERC721A.sol +++ b/contracts/IERC721A.sol @@ -74,6 +74,11 @@ interface IERC721A { */ error OwnershipNotInitializedForExtraData(); + /** + * The `tokenIds` must be strictly ascending. + */ + error TokenIdsNotStrictlyAscending(); + // ============================================================= // STRUCTS // ============================================================= diff --git a/contracts/mocks/ERC721ABatchBurnableMock.sol b/contracts/mocks/ERC721ABatchBurnableMock.sol new file mode 100644 index 000000000..d7d26061a --- /dev/null +++ b/contracts/mocks/ERC721ABatchBurnableMock.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v4.2.3 +// Creators: Chiru Labs + +pragma solidity ^0.8.4; + +import '../ERC721A.sol'; +import './DirectBurnBitSetterHelper.sol'; + +contract ERC721ABatchBurnableMock is ERC721A, DirectBurnBitSetterHelper { + constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {} + + function exists(uint256 tokenId) public view returns (bool) { + return _exists(tokenId); + } + + function safeMint(address to, uint256 quantity) public { + _safeMint(to, quantity); + } + + function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) { + return _ownershipAt(index); + } + + function totalMinted() public view returns (uint256) { + return _totalMinted(); + } + + function totalBurned() public view returns (uint256) { + return _totalBurned(); + } + + function numberBurned(address owner) public view returns (uint256) { + return _numberBurned(owner); + } + + function bulkBurn(address burner, uint256[] memory tokenIds, bool approvalCheck) public { + _batchBurn(burner, tokenIds, approvalCheck); + } +} From 411cf397d18ba6cdc7c9f6e7fc1b5c41cac74416 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sat, 25 Mar 2023 18:20:14 +0100 Subject: [PATCH 53/74] change tokenIds in ascending order in test --- test/extensions/ERC721ABatchBurnable.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/extensions/ERC721ABatchBurnable.test.js b/test/extensions/ERC721ABatchBurnable.test.js index 170c7ebe0..73ca794fa 100644 --- a/test/extensions/ERC721ABatchBurnable.test.js +++ b/test/extensions/ERC721ABatchBurnable.test.js @@ -25,7 +25,7 @@ const createTestSuite = ({ contract, constructorArgs }) => this.numTestTokens = 20; this.totalBurned = 6; this.burnedTokenIds1 = [2, 3, 4]; - this.burnedTokenIds2 = [7, 10, 9]; + this.burnedTokenIds2 = [7, 9, 10]; this.notBurnedTokenId1 = 1; this.notBurnedTokenId2 = 5; this.notBurnedTokenId3 = 6; @@ -36,7 +36,7 @@ const createTestSuite = ({ contract, constructorArgs }) => await this.erc721aBatchBurnable['safeMint(address,uint256)'](this.addr1.address, this.numTestTokens); await this.erc721aBatchBurnable .connect(this.addr1) - .batchBurn([...this.burnedTokenIds2, ...this.burnedTokenIds1]); + .batchBurn([...this.burnedTokenIds1, ...this.burnedTokenIds2]); // Manually initialize `this.initializedToken` await this.erc721aBatchBurnable.initializeOwnershipAt(this.initializedToken); From 7a28bf0d393bdf0593e69952d9cbb5630126fcb6 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sat, 25 Mar 2023 18:27:51 +0100 Subject: [PATCH 54/74] removal of unneeded internal functions --- contracts/ERC721A.sol | 57 ++++++------------------------------------- 1 file changed, 7 insertions(+), 50 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 377e8783f..81ddadf1c 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1212,49 +1212,6 @@ contract ERC721A is IERC721A { emit Approval(owner, to, tokenId); } - /** - * @dev Private function to handle clearing approvals and emitting transfer Event for a given `tokenId`. - * Used in `_batchTransferFrom`. - * - * `from` - Previous owner of the given token ID. - * `toMasked` - Target address that will receive the token. - * `tokenId` - Token ID to be transferred. - * `isApprovedForAll_` - Whether the caller is approved for all token IDs. - */ - function _clearApprovalsAndEmitTransferEvent( - address from, - uint256 toMasked, - uint256 tokenId, - bool approvalCheck - ) private { - (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); - - if (approvalCheck) { - if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) - _revert(TransferCallerNotOwnerNorApproved.selector); - } - - // Clear approvals from the previous owner. - assembly { - if approvedAddress { - // This is equivalent to `delete _tokenApprovals[tokenId]`. - sstore(approvedAddressSlot, 0) - } - } - - assembly { - // Emit the `Transfer` event. - log4( - 0, // Start of data (0, since no data). - 0, // End of data (0, since no data). - _TRANSFER_EVENT_SIGNATURE, // Signature. - from, // `from`. - toMasked, // `to`. - tokenId // `tokenId`. - ) - } - } - // ============================================================= // BURN OPERATIONS // ============================================================= @@ -1334,6 +1291,13 @@ contract ERC721A is IERC721A { } } + /** + * @dev Equivalent to `_batchBurn(tokenIds, false)`. + */ + function _batchBurn(uint256[] memory tokenIds) internal virtual { + _batchBurn(tokenIds, false); + } + /** * @dev Destroys `tokenIds`. * The approval is cleared when the token is burned. @@ -1435,13 +1399,6 @@ contract ERC721A is IERC721A { } } - /** - * @dev Equivalent to `_batchBurn(tokenIds, false)`. - */ - function _batchBurn(uint256[] memory tokenIds) internal virtual { - _batchBurn(tokenIds, false); - } - // ============================================================= // EXTRA DATA OPERATIONS // ============================================================= From 8e2fc6af25d6a5930b07f335f3208c22ddb5f63b Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sat, 25 Mar 2023 18:28:39 +0100 Subject: [PATCH 55/74] prettier --- contracts/ERC721A.sol | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 81ddadf1c..4f87e35e6 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1310,7 +1310,11 @@ contract ERC721A is IERC721A { * * Emits a {Transfer} event for each token burned. */ - function _batchBurn(address burner, uint256[] memory tokenIds, bool approvalCheck) internal virtual { + function _batchBurn( + address burner, + uint256[] memory tokenIds, + bool approvalCheck + ) internal virtual { // We can use unchecked as the length of `tokenIds` is bounded // to a small number by the max block gas limit. unchecked { @@ -1329,7 +1333,7 @@ contract ERC721A is IERC721A { if (_or(tokenId < _startTokenId(), stop <= tokenId)) revert OwnerQueryForNonexistentToken(); // Revert if `tokenIds` is not strictly ascending. - if (i != 0) + if (i != 0) if (tokenId <= prevTokenId) revert TokenIdsNotStrictlyAscending(); // The initialized packed ownership slot's value. @@ -1372,15 +1376,14 @@ contract ERC721A is IERC721A { ); // Update the packed ownership for `tokenId` in ERC721A's storage. - _packedOwnerships[tokenId] = - _BITMASK_BURNED | - (block.timestamp << _BITPOS_START_TIMESTAMP) | + _packedOwnerships[tokenId] = + _BITMASK_BURNED | + (block.timestamp << _BITPOS_START_TIMESTAMP) | uint256(uint160(tokenOwner)); // If the slot after the mini batch is neither out of bounds, nor initialized. if (currTokenId != stop) - if (_packedOwnerships[currTokenId] == 0) - _packedOwnerships[currTokenId] = prevOwnershipPacked; + if (_packedOwnerships[currTokenId] == 0) _packedOwnerships[currTokenId] = prevOwnershipPacked; // Update the address data in ERC721A's storage. // From 9dac917d11eb4e08b002555103579940b1b0adf2 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sat, 25 Mar 2023 18:52:26 +0100 Subject: [PATCH 56/74] removed batch transfer logic --- contracts/ERC721A.sol | 256 --------- .../extensions/ERC721ABatchTransferable.sol | 40 -- .../extensions/IERC721ABatchTransferable.sol | 62 -- .../interfaces/IERC721ABatchTransferable.sol | 7 - .../mocks/ERC721ABatchTransferableMock.sol | 69 --- .../ERC721ABatchTransferable.test.js | 529 ------------------ 6 files changed, 963 deletions(-) delete mode 100644 contracts/extensions/ERC721ABatchTransferable.sol delete mode 100644 contracts/extensions/IERC721ABatchTransferable.sol delete mode 100644 contracts/interfaces/IERC721ABatchTransferable.sol delete mode 100644 contracts/mocks/ERC721ABatchTransferableMock.sol delete mode 100644 test/extensions/ERC721ABatchTransferable.test.js diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 4f87e35e6..2df8c8175 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -648,231 +648,10 @@ contract ERC721A is IERC721A { } } - /** - * @dev Equivalent to `_batchTransferFrom(from, to, tokenIds, false)`. - */ - function _batchTransferFrom( - address from, - address to, - uint256[] memory tokenIds - ) internal virtual { - _batchTransferFrom(from, to, tokenIds, false); - } - - /** - * @dev Transfers `tokenIds` in batch from `from` to `to`. - * `tokenIds` should be provided sorted in ascending order to maximize efficiency. - * - * Requirements: - * - * - `from` cannot be the zero address. - * - `to` cannot be the zero address. - * - `tokenIds` tokens must be owned by `from`. - * - If the caller is not `from`, it must be approved to move these tokens - * by either {approve} or {setApprovalForAll}. - * - * Emits a {Transfer} event for each transfer. - */ - function _batchTransferFrom( - address from, - address to, - uint256[] memory tokenIds, - bool approvalCheck - ) internal virtual { - // Sort `tokenIds` to allow batching consecutive ids. - _sort(tokenIds); - - // Mask `from` and `to` to the lower 160 bits, in case the upper bits somehow aren't clean. - from = address(uint160(uint256(uint160(from)) & _BITMASK_ADDRESS)); - uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS; - if (toMasked == 0) _revert(TransferToZeroAddress.selector); - - uint256 totalTokens = tokenIds.length; - uint256 totalTokensLeft; - uint256 startTokenId; - uint256 nextTokenId; - uint256 prevOwnershipPacked; - uint256 nextOwnershipPacked; - uint256 quantity; - - // If `approvalCheck` is true, check if the caller is approved for all token Ids. - // If approved for all, disable next approval checks. Otherwise keep them enabled - if (approvalCheck) - if (isApprovedForAll(from, _msgSenderERC721A())) approvalCheck = false; - - _beforeTokenBatchTransfers(from, to, tokenIds); - - // Underflow of the sender's balance is temporarily possible if the wrong set of token Ids is passed, - // but reverts afterwards when ownership is checked. - // The recipient's balance can't realistically overflow. - // Counter overflow is incredibly unrealistic as `startTokenId` would have to be 2**256. - unchecked { - // We can directly increment and decrement the balances. - _packedAddressData[from] -= totalTokens; - _packedAddressData[to] += totalTokens; - - for (uint256 i; i < totalTokens; ) { - startTokenId = tokenIds[i]; - totalTokensLeft = totalTokens - i; - - // Updates `prevOwnershipPacked` and check ownership of `startTokenId`. - prevOwnershipPacked = _packedOwnershipOf(startTokenId); - if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); - - // Updates startTokenId: - // - `address` to the next owner. - // - `startTimestamp` to the timestamp of transfering. - // - `burned` to `false`. - // - `nextInitialized` to `false`, to account for potential clearing of `startTokenId + 1`. - _packedOwnerships[startTokenId] = _packOwnershipData(to, _nextExtraData(from, to, prevOwnershipPacked)); - - // Clear approvals and emit transfer event for `startTokenId`. - _clearApprovalsAndEmitTransferEvent(from, toMasked, startTokenId, approvalCheck); - - // Derive quantity by looping over the next consecutive `totalTokensLeft`. - for (quantity = 1; quantity < totalTokensLeft; ++quantity) { - nextTokenId = startTokenId + quantity; - - // If `nextTokenId` is not consecutive, update `nextTokenId` and break from the loop. - if (tokenIds[i + quantity] != nextTokenId) { - _updateTokenId( - nextTokenId, - quantity == 1 ? prevOwnershipPacked : nextOwnershipPacked, - prevOwnershipPacked - ); - break; - } - - nextOwnershipPacked = _packedOwnerships[nextTokenId]; - - // If the next slot's address is zero and not burned (i.e. packed value is zero). - if (nextOwnershipPacked == 0) { - // Revert if the next slot is out of bounds. Cannot be higher than `_currentIndex` since we're - // incrementing in steps of one - if (nextTokenId == _currentIndex) _revert(OwnerQueryForNonexistentToken.selector); - // Otherwise we assume `from` owns `nextTokenId` and move on. - } else { - // Revert if `nextTokenId` is not owned by `from` or has been burned. - if (address(uint160(nextOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); - if (nextOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); - - // If there is extra data to preserve - if (nextOwnershipPacked >> _BITPOS_EXTRA_DATA != 0) { - // Updates `prevOwnershipPacked` with last initialized `nextOwnershipPacked`. - prevOwnershipPacked = nextOwnershipPacked; - - // Updates `nextTokenId` - // - `address` to the next owner. - // - `startTimestamp` to the timestamp of transfering. - // - `burned` to `false`. - // - `nextInitialized` to `false`, to account for potential clearing of `nextTokenId + 1`. - _packedOwnerships[nextTokenId] = _packOwnershipData( - to, - (nextOwnershipPacked & _BITMASK_NEXT_INITIALIZED) | - _nextExtraData(from, to, nextOwnershipPacked) - ); - } else { - // Otherwise clear slot of `nextTokenId` to leverage gas optimization of consecutive ids - delete _packedOwnerships[nextTokenId]; - } - } - - // Clear approvals and emit transfer event for `nextTokenId`. - _clearApprovalsAndEmitTransferEvent(from, toMasked, nextTokenId, approvalCheck); - } - - // Skip the next `quantity` tokens. - i += quantity; - } - - // Update the tokenId subsequent to the last element in `tokenIds` - if (quantity == 1) { - _updateTokenId(startTokenId + 1, prevOwnershipPacked, prevOwnershipPacked); - } else { - _updateTokenId(nextTokenId + 1, nextOwnershipPacked, prevOwnershipPacked); - } - } - - _afterTokenBatchTransfers(from, to, tokenIds); - } - - /** - * @dev Equivalent to `_safeBatchTransferFrom(from, to, tokenIds, false)`. - */ - function _safeBatchTransferFrom( - address from, - address to, - uint256[] memory tokenIds - ) internal virtual { - _safeBatchTransferFrom(from, to, tokenIds, false); - } - - /** - * @dev Equivalent to `_safeBatchTransferFrom(from, to, tokenIds, '', approvalCheck)`. - */ - function _safeBatchTransferFrom( - address from, - address to, - uint256[] memory tokenIds, - bool approvalCheck - ) internal virtual { - _safeBatchTransferFrom(from, to, tokenIds, '', approvalCheck); - } - - /** - * @dev Equivalent to `_safeBatchTransferFrom(from, to, tokenIds, _data, false)`. - */ - function _safeBatchTransferFrom( - address from, - address to, - uint256[] memory tokenIds, - bytes memory _data - ) internal virtual { - _safeBatchTransferFrom(from, to, tokenIds, _data, false); - } - - /** - * @dev Safely transfers `tokenIds` in batch from `from` to `to`. - * - * Requirements: - * - * - `from` cannot be the zero address. - * - `to` cannot be the zero address. - * - `tokenIds` tokens must be owned by `from`. - * - If the caller is not `from`, it must be approved to move these tokens - * by either {approve} or {setApprovalForAll}. - * - If `to` refers to a smart contract, it must implement - * {IERC721Receiver-onERC721Received}, which is called for each transferred token. - * - * Emits a {Transfer} event for each transfer. - */ - function _safeBatchTransferFrom( - address from, - address to, - uint256[] memory tokenIds, - bytes memory _data, - bool approvalCheck - ) internal virtual { - _batchTransferFrom(from, to, tokenIds, approvalCheck); - - uint256 tokenId; - uint256 totalTokens = tokenIds.length; - unchecked { - for (uint256 i; i < totalTokens; ++i) { - tokenId = tokenIds[i]; - if (to.code.length != 0) - if (!_checkContractOnERC721Received(from, to, tokenId, _data)) { - _revert(TransferToNonERC721ReceiverImplementer.selector); - } - } - } - } - /** * @dev Hook that is called before a set of serially-ordered token IDs * are about to be transferred. This includes minting. * And also called before burning one token. - * But not called on batch transfers and burns (see `_beforeTokenBatchTransfers`). * * `startTokenId` - the first token ID to be transferred. * `quantity` - the amount to be transferred. @@ -896,7 +675,6 @@ contract ERC721A is IERC721A { * @dev Hook that is called after a set of serially-ordered token IDs * have been transferred. This includes minting. * And also called after one token has been burned. - * But not called on batch transfers and burns (see `_afterTokenBatchTransfers`). * * `startTokenId` - the first token ID to be transferred. * `quantity` - the amount to be transferred. @@ -916,40 +694,6 @@ contract ERC721A is IERC721A { uint256 quantity ) internal virtual {} - /** - * @dev Hook that is called before a set of token IDs ordered in ascending order - * are about to be transferred. Only called on batch transfers and burns. - * - * `tokenIds` - the array of tokenIds to be transferred, ordered in ascending order. - * - * Calling conditions: - * - * - `from`'s `tokenIds` will be transferred to `to`. - * - Neither `from` and `to` can be zero. - */ - function _beforeTokenBatchTransfers( - address from, - address to, - uint256[] memory tokenIds - ) internal virtual {} - - /** - * @dev Hook that is called after a set of token IDs ordered in ascending order - * have been transferred. Only called on batch transfers and burns. - * - * `tokenIds` - the array of tokenIds transferred, ordered in ascending order. - * - * Calling conditions: - * - * - `from`'s `tokenIds` have been transferred to `to`. - * - Neither `from` and `to` can be zero. - */ - function _afterTokenBatchTransfers( - address from, - address to, - uint256[] memory tokenIds - ) internal virtual {} - /** * @dev Private function to invoke {IERC721Receiver-onERC721Received} on a target contract. * diff --git a/contracts/extensions/ERC721ABatchTransferable.sol b/contracts/extensions/ERC721ABatchTransferable.sol deleted file mode 100644 index d4caa4a0a..000000000 --- a/contracts/extensions/ERC721ABatchTransferable.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MIT -// ERC721A Contracts v4.2.3 -// Creator: Chiru Labs - -pragma solidity ^0.8.4; - -import '../ERC721A.sol'; -import './IERC721ABatchTransferable.sol'; - -/** - * @title ERC721ABatchTransferable. - * - * @dev ERC721A token optimized for batch transfers. - */ -abstract contract ERC721ABatchTransferable is ERC721A, IERC721ABatchTransferable { - function batchTransferFrom( - address from, - address to, - uint256[] memory tokenIds - ) public payable virtual override { - _batchTransferFrom(from, to, tokenIds, true); - } - - function safeBatchTransferFrom( - address from, - address to, - uint256[] memory tokenIds - ) public payable virtual override { - _safeBatchTransferFrom(from, to, tokenIds, true); - } - - function safeBatchTransferFrom( - address from, - address to, - uint256[] memory tokenIds, - bytes memory _data - ) public payable virtual override { - _safeBatchTransferFrom(from, to, tokenIds, _data, true); - } -} diff --git a/contracts/extensions/IERC721ABatchTransferable.sol b/contracts/extensions/IERC721ABatchTransferable.sol deleted file mode 100644 index 0b984e16b..000000000 --- a/contracts/extensions/IERC721ABatchTransferable.sol +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: MIT -// ERC721A Contracts v4.2.3 -// Creator: Chiru Labs - -pragma solidity ^0.8.4; - -import '../IERC721A.sol'; - -/** - * @dev Interface of ERC721ABatchTransferable. - */ -interface IERC721ABatchTransferable is IERC721A { - /** - * @dev Transfers `tokenIds` in batch from `from` to `to`. See {ERC721A-_batchTransferFrom}. - * - * Requirements: - * - * - `from` cannot be the zero address. - * - `to` cannot be the zero address. - * - `tokenIds` tokens must be owned by `from`. - * - If the caller is not `from`, it must be approved to move these tokens - * by either {approve} or {setApprovalForAll}. - * - * Emits a {Transfer} event for each transfer. - */ - function batchTransferFrom( - address from, - address to, - uint256[] memory tokenIds - ) external payable; - - /** - * @dev Equivalent to `safeBatchTransferFrom(from, to, tokenIds, '')`. - */ - function safeBatchTransferFrom( - address from, - address to, - uint256[] memory tokenIds - ) external payable; - - /** - * @dev Safely transfers `tokenIds` in batch from `from` to `to`. See {ERC721A-_safeBatchTransferFrom}. - * - * Requirements: - * - * - `from` cannot be the zero address. - * - `to` cannot be the zero address. - * - `tokenIds` tokens must be owned by `from`. - * - If the caller is not `from`, it must be approved to move these tokens - * by either {approve} or {setApprovalForAll}. - * - If `to` refers to a smart contract, it must implement - * {IERC721Receiver-onERC721Received}, which is called for each transferred token. - * - * Emits a {Transfer} event for each transfer. - */ - function safeBatchTransferFrom( - address from, - address to, - uint256[] memory tokenIds, - bytes memory _data - ) external payable; -} diff --git a/contracts/interfaces/IERC721ABatchTransferable.sol b/contracts/interfaces/IERC721ABatchTransferable.sol deleted file mode 100644 index bde71ccae..000000000 --- a/contracts/interfaces/IERC721ABatchTransferable.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -// ERC721A Contracts v4.2.3 -// Creator: Chiru Labs - -pragma solidity ^0.8.4; - -import '../extensions/IERC721ABatchTransferable.sol'; diff --git a/contracts/mocks/ERC721ABatchTransferableMock.sol b/contracts/mocks/ERC721ABatchTransferableMock.sol deleted file mode 100644 index d0aaea814..000000000 --- a/contracts/mocks/ERC721ABatchTransferableMock.sol +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: MIT -// ERC721A Contracts v4.2.3 -// Creators: Chiru Labs - -pragma solidity ^0.8.4; - -import '../extensions/ERC721ABatchTransferable.sol'; - -contract ERC721ABatchTransferableMock is ERC721ABatchTransferable { - constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {} - - function exists(uint256 tokenId) public view returns (bool) { - return _exists(tokenId); - } - - function safeMint(address to, uint256 quantity) public { - _safeMint(to, quantity); - } - - function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) { - return _ownershipAt(index); - } - - function totalMinted() public view returns (uint256) { - return _totalMinted(); - } - - function totalBurned() public view returns (uint256) { - return _totalBurned(); - } - - function numberBurned(address owner) public view returns (uint256) { - return _numberBurned(owner); - } - - function burn(uint256 tokenId) public { - _burn(tokenId, true); - } - - function initializeOwnershipAt(uint256 index) public { - _initializeOwnershipAt(index); - } - - function _extraData( - address, - address, - uint24 previousExtraData - ) internal view virtual override returns (uint24) { - return previousExtraData; - } - - function setExtraDataAt(uint256 index, uint24 extraData) public { - _setExtraDataAt(index, extraData); - } - - function batchTransferFromUnoptimized( - address from, - address to, - uint256[] memory tokenIds - ) public { - unchecked { - uint256 tokenId; - for (uint256 i; i < tokenIds.length; ++i) { - tokenId = tokenIds[i]; - transferFrom(from, to, tokenId); - } - } - } -} diff --git a/test/extensions/ERC721ABatchTransferable.test.js b/test/extensions/ERC721ABatchTransferable.test.js deleted file mode 100644 index 38ce7c38c..000000000 --- a/test/extensions/ERC721ABatchTransferable.test.js +++ /dev/null @@ -1,529 +0,0 @@ -const { deployContract, getBlockTimestamp, mineBlockTimestamp, offsettedIndex } = require('../helpers.js'); -const { expect } = require('chai'); -const { constants } = require('@openzeppelin/test-helpers'); -const { ZERO_ADDRESS } = constants; - -const RECEIVER_MAGIC_VALUE = '0x150b7a02'; - -const createTestSuite = ({ contract, constructorArgs }) => - function () { - let offsetted; - - context(`${contract}`, function () { - beforeEach(async function () { - this.erc721aBatchTransferable = await deployContract(contract, constructorArgs); - this.receiver = await deployContract('ERC721ReceiverMock', [ - RECEIVER_MAGIC_VALUE, - this.erc721aBatchTransferable.address, - ]); - this.startTokenId = this.erc721aBatchTransferable.startTokenId - ? (await this.erc721aBatchTransferable.startTokenId()).toNumber() - : 0; - - offsetted = (...arr) => offsettedIndex(this.startTokenId, arr); - offsetted(0); - }); - - beforeEach(async function () { - const [owner, addr1, addr2, addr3, addr4, addr5] = await ethers.getSigners(); - this.owner = owner; - this.addr1 = addr1; - this.addr2 = addr2; - this.addr3 = addr3; - this.addr4 = addr4; - this.addr5 = addr5; - - this.addr1.expected = { - mintCount: 3, - tokens: offsetted(2, 4, 5), - }; - - this.addr2.expected = { - mintCount: 20, - tokens: offsetted(0, 1, 17, 6, 7, 21, 13, 19, 10, 12, 11, 8, 20, 14, 15, 16, 3, 18, 22, 9), - }; - - this.addr3.expected = { - mintCount: 7, - tokens: offsetted(23, 24, 25, 26, 27, 28, 29), - }; - - this.numTotalTokens = - this.addr1.expected.mintCount + this.addr2.expected.mintCount + this.addr3.expected.mintCount; - - await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr2.address, 2); - await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr1.address, 1); - await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr2.address, 1); - await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr1.address, 2); - await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr2.address, 17); - await this.erc721aBatchTransferable['safeMint(address,uint256)'](this.addr3.address, 7); - }); - - context('test batch transfer functionality', function () { - const testSuccessfulBatchTransfer = function (transferFn, transferToContract = true) { - describe('successful transfers', async function () { - beforeEach(async function () { - const sender = this.addr2; - this.tokenIds = this.addr2.expected.tokens; - this.from = sender.address; - this.to = transferToContract ? this.receiver : this.addr4; - this.approvedIds = [this.tokenIds[2], this.tokenIds[3]]; - this.initializedToken = 8; - this.uninitializedToken = 10; - - this.approvedIds.forEach(async (tokenId) => { - await this.erc721aBatchTransferable.connect(sender).approve(this.to.address, tokenId); - }); - - // Manually initialize `this.initializedToken` - await this.erc721aBatchTransferable.initializeOwnershipAt(this.initializedToken); - - const ownershipBefore = await this.erc721aBatchTransferable.getOwnershipAt(3); - this.timestampBefore = parseInt(ownershipBefore.startTimestamp); - this.timestampToMine = (await getBlockTimestamp()) + 12345; - await mineBlockTimestamp(this.timestampToMine); - this.timestampMined = await getBlockTimestamp(); - - // prettier-ignore - this.transferTx = await this.erc721aBatchTransferable - .connect(sender)[transferFn](this.from, this.to.address, this.tokenIds); - - const ownershipAfter = await this.erc721aBatchTransferable.getOwnershipAt(3); - this.timestampAfter = parseInt(ownershipAfter.startTimestamp); - - // Transfer part of uninitialized tokens - this.tokensToTransferAlt = [25, 26, 27]; - // prettier-ignore - this.transferTxAlt = await this.erc721aBatchTransferable.connect(this.addr3)[transferFn]( - this.addr3.address, this.addr5.address, this.tokensToTransferAlt - ); - }); - - it('emits Transfers event', async function () { - for (let i = 0; i < this.tokenIds.length; i++) { - const tokenId = this.tokenIds[i]; - await expect(this.transferTx) - .to.emit(this.erc721aBatchTransferable, 'Transfer') - .withArgs(this.from, this.to.address, tokenId); - } - }); - - it('adjusts owners balances', async function () { - expect(await this.erc721aBatchTransferable.balanceOf(this.from)).to.be.equal(0); - expect(await this.erc721aBatchTransferable.balanceOf(this.to.address)).to.be.equal( - this.addr2.expected.mintCount - ); - expect(await this.erc721aBatchTransferable.balanceOf(this.addr3.address)).to.be.equal( - this.addr3.expected.tokens.length - this.tokensToTransferAlt.length - ); - expect(await this.erc721aBatchTransferable.balanceOf(this.addr5.address)).to.be.equal( - this.tokensToTransferAlt.length - ); - }); - - it('clears the approval for the token IDs', async function () { - this.approvedIds.forEach(async (tokenId) => { - expect(await this.erc721aBatchTransferable.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS); - }); - }); - - it('startTimestamp updated correctly', async function () { - expect(this.timestampBefore).to.be.lt(this.timestampToMine); - expect(this.timestampAfter).to.be.gte(this.timestampToMine); - expect(this.timestampAfter).to.be.lt(this.timestampToMine + 10); - expect(this.timestampToMine).to.be.eq(this.timestampMined); - }); - - it('with transfer of the given token IDs to the given address', async function () { - for (let i = 0; i < this.tokenIds.length; i++) { - const tokenId = this.tokenIds[i]; - expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.to.address); - } - - // Initialized tokens were updated - expect((await this.erc721aBatchTransferable.getOwnershipAt(3))[0]).to.be.equal(this.to.address); - - // Initialized tokens in a consecutive transfer are cleared - expect((await this.erc721aBatchTransferable.getOwnershipAt(8))[0]).to.be.equal( - transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.to.address - ); - - // Uninitialized tokens are left uninitialized - expect((await this.erc721aBatchTransferable.getOwnershipAt(7))[0]).to.be.equal( - transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.to.address - ); - - // Other tokens in between are left unchanged - for (let i = 0; i < this.addr1.expected.tokens.length; i++) { - const tokenId = this.addr1.expected.tokens[i]; - expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr1.address); - } - }); - - it('with transfers of uninitialized token IDs to the given address', async function () { - const allTokensInitiallyOwned = this.addr3.expected.tokens; - allTokensInitiallyOwned.splice(2, this.tokensToTransferAlt.length); - - for (let i = 0; i < this.tokensToTransferAlt.length; i++) { - const tokenId = this.tokensToTransferAlt[i]; - expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr5.address); - } - - for (let i = 0; i < allTokensInitiallyOwned.length; i++) { - const tokenId = allTokensInitiallyOwned[i]; - expect(await this.erc721aBatchTransferable.ownerOf(tokenId)).to.be.equal(this.addr3.address); - } - - // Ownership of tokens was updated - expect((await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[0]))[0]).to.be.equal( - this.addr5.address - ); - expect((await this.erc721aBatchTransferable.getOwnershipAt(allTokensInitiallyOwned[2]))[0]).to.be.equal( - this.addr3.address - ); - - // Uninitialized tokens are left uninitialized - expect( - (await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[0] - 1))[0] - ).to.be.equal(ZERO_ADDRESS); - expect((await this.erc721aBatchTransferable.getOwnershipAt(allTokensInitiallyOwned[3]))[0]).to.be.equal( - ZERO_ADDRESS - ); - expect((await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[1]))[0]).to.be.equal( - transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.addr5.address - ); - expect((await this.erc721aBatchTransferable.getOwnershipAt(this.tokensToTransferAlt[2]))[0]).to.be.equal( - transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.addr5.address - ); - }); - }); - - describe('ownership correctly set', async function () { - beforeEach(async function () { - const sender = this.addr2; - this.from = sender.address; - this.to = transferToContract ? this.receiver : this.addr4; - this.initializedToken = 8; - this.uninitializedToken = 10; - - // Manually initialize some tokens of addr2 - await this.erc721aBatchTransferable.initializeOwnershipAt(this.initializedToken); - }); - - it('with tokens transferred and cleared', async function () { - const initializedToken = 15; - - expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( - ZERO_ADDRESS - ); - expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken))[0]).to.be.equal( - ZERO_ADDRESS - ); - - // Initialize token - await this.erc721aBatchTransferable.initializeOwnershipAt(initializedToken); - expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( - ZERO_ADDRESS - ); - expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken))[0]).to.be.equal( - this.addr2.address - ); - - // Transfer tokens - // prettier-ignore - await this.erc721aBatchTransferable - .connect(this.addr2)[transferFn]( - this.from, this.to.address, [initializedToken - 1, initializedToken] - ); - expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( - this.to.address - ); - - // Initialized tokens in a consecutive transfer are cleared - expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken))[0]).to.be.equal( - transferFn !== 'batchTransferFromUnoptimized' ? ZERO_ADDRESS : this.to.address - ); - }); - - it('with tokens transferred and updated', async function () { - const initializedToken = 15; - const extraData = 123; - - expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( - ZERO_ADDRESS - ); - expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken))[0]).to.be.equal( - ZERO_ADDRESS - ); - - // Initialize token - await this.erc721aBatchTransferable.initializeOwnershipAt(initializedToken); - expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( - ZERO_ADDRESS - ); - expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken))[0]).to.be.equal( - this.addr2.address - ); - - // Set extra data - await this.erc721aBatchTransferable.setExtraDataAt(initializedToken, extraData); - expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken))[3]).to.be.equal(extraData); - - // Transfer tokens - // prettier-ignore - await this.erc721aBatchTransferable - .connect(this.addr2)[transferFn]( - this.from, this.to.address, [initializedToken - 1, initializedToken] - ); - expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( - this.to.address - ); - - // Initialized tokens in a consecutive transfer are updated when nextData is not 0 - expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken))[0]).to.be.equal( - this.to.address - ); - expect((await this.erc721aBatchTransferable.getOwnershipAt(initializedToken))[3]).to.be.equal(extraData); - }); - - it('with first token transferred', async function () { - expect(await this.erc721aBatchTransferable.ownerOf(0)).to.be.equal(this.from); - expect(await this.erc721aBatchTransferable.ownerOf(1)).to.be.equal(this.from); - expect((await this.erc721aBatchTransferable.getOwnershipAt(0))[0]).to.be.equal(this.from); - expect((await this.erc721aBatchTransferable.getOwnershipAt(1))[0]).to.be.equal(ZERO_ADDRESS); - - // prettier-ignore - await this.erc721aBatchTransferable - .connect(this.addr2)[transferFn](this.from, this.to.address, [0]); - - expect(await this.erc721aBatchTransferable.ownerOf(0)).to.be.equal(this.to.address); - expect(await this.erc721aBatchTransferable.ownerOf(1)).to.be.equal(this.from); - expect((await this.erc721aBatchTransferable.getOwnershipAt(0))[0]).to.be.equal(this.to.address); - expect((await this.erc721aBatchTransferable.getOwnershipAt(1))[0]).to.be.equal(this.from); - }); - - it('with last token transferred', async function () { - await expect(this.erc721aBatchTransferable.ownerOf(this.numTotalTokens)).to.be.revertedWith( - 'OwnerQueryForNonexistentToken' - ); - - // prettier-ignore - await this.erc721aBatchTransferable - .connect(this.addr3)[transferFn]( - this.addr3.address, this.to.address, [offsetted(this.numTotalTokens - 1 - )]); - - expect(await this.erc721aBatchTransferable.ownerOf(offsetted(this.numTotalTokens - 1))).to.be.equal( - this.to.address - ); - await expect(this.erc721aBatchTransferable.ownerOf(this.numTotalTokens)).to.be.revertedWith( - 'OwnerQueryForNonexistentToken' - ); - }); - - it('with initialized token transferred', async function () { - expect(await this.erc721aBatchTransferable.ownerOf(this.initializedToken)).to.be.equal(this.from); - expect(await this.erc721aBatchTransferable.ownerOf(this.initializedToken + 1)).to.be.equal(this.from); - expect((await this.erc721aBatchTransferable.getOwnershipAt(this.initializedToken))[0]).to.be.equal( - this.from - ); - expect((await this.erc721aBatchTransferable.getOwnershipAt(this.initializedToken + 1))[0]).to.be.equal( - ZERO_ADDRESS - ); - - // prettier-ignore - await this.erc721aBatchTransferable - .connect(this.addr2)[transferFn](this.from, this.to.address, [this.initializedToken]); - - expect(await this.erc721aBatchTransferable.ownerOf(this.initializedToken)).to.be.equal(this.to.address); - expect(await this.erc721aBatchTransferable.ownerOf(this.initializedToken + 1)).to.be.equal(this.from); - expect((await this.erc721aBatchTransferable.getOwnershipAt(this.initializedToken))[0]).to.be.equal( - this.to.address - ); - expect((await this.erc721aBatchTransferable.getOwnershipAt(this.initializedToken + 1))[0]).to.be.equal( - this.from - ); - }); - - it('with uninitialized token transferred', async function () { - expect(await this.erc721aBatchTransferable.ownerOf(this.uninitializedToken)).to.be.equal(this.from); - expect(await this.erc721aBatchTransferable.ownerOf(this.uninitializedToken + 1)).to.be.equal(this.from); - expect((await this.erc721aBatchTransferable.getOwnershipAt(this.uninitializedToken))[0]).to.be.equal( - ZERO_ADDRESS - ); - expect((await this.erc721aBatchTransferable.getOwnershipAt(this.uninitializedToken + 1))[0]).to.be.equal( - ZERO_ADDRESS - ); - - // prettier-ignore - await this.erc721aBatchTransferable - .connect(this.addr2)[transferFn](this.from, this.to.address, [this.uninitializedToken]); - - expect(await this.erc721aBatchTransferable.ownerOf(this.uninitializedToken)).to.be.equal(this.to.address); - expect(await this.erc721aBatchTransferable.ownerOf(this.uninitializedToken + 1)).to.be.equal(this.from); - expect((await this.erc721aBatchTransferable.getOwnershipAt(this.uninitializedToken))[0]).to.be.equal( - this.to.address - ); - expect((await this.erc721aBatchTransferable.getOwnershipAt(this.uninitializedToken + 1))[0]).to.be.equal( - this.from - ); - }); - }); - }; - - const testUnsuccessfulBatchTransfer = function (transferFn) { - describe('unsuccessful transfers', function () { - beforeEach(function () { - this.tokenIds = this.addr2.expected.tokens.slice(0, 2); - this.sender = this.addr1; - }); - - it('rejects unapproved transfer', async function () { - // prettier-ignore - await expect( - this.erc721aBatchTransferable - .connect(this.sender)[transferFn]( - this.addr2.address, this.sender.address, this.tokenIds - ) - ).to.be.revertedWith('TransferCallerNotOwnerNorApproved'); - }); - - it('rejects transfer from incorrect owner', async function () { - await this.erc721aBatchTransferable.connect(this.addr2).setApprovalForAll(this.sender.address, true); - // prettier-ignore - await expect( - this.erc721aBatchTransferable - .connect(this.sender)[transferFn]( - this.addr3.address, this.sender.address, this.tokenIds - ) - ).to.be.revertedWith('TransferFromIncorrectOwner'); - }); - - it('rejects transfer to zero address', async function () { - await this.erc721aBatchTransferable.connect(this.addr2).setApprovalForAll(this.sender.address, true); - // prettier-ignore - await expect( - this.erc721aBatchTransferable - .connect(this.sender)[transferFn]( - this.addr2.address, ZERO_ADDRESS, this.tokenIds - ) - ).to.be.revertedWith('TransferToZeroAddress'); - }); - }); - }; - - const testApproveBatchTransfer = function (transferFn) { - describe('approvals correctly set', async function () { - beforeEach(function () { - this.tokenIds = this.addr1.expected.tokens.slice(0, 2); - }); - - it('approval allows batch transfers', async function () { - // prettier-ignore - await expect( - this.erc721aBatchTransferable - .connect(this.addr3)[transferFn]( - this.addr1.address, this.addr3.address, this.tokenIds - ) - ).to.be.revertedWith('TransferCallerNotOwnerNorApproved'); - - for (let i = 0; i < this.tokenIds.length; i++) { - const tokenId = this.tokenIds[i]; - await this.erc721aBatchTransferable.connect(this.addr1).approve(this.addr3.address, tokenId); - } - - // prettier-ignore - await this.erc721aBatchTransferable - .connect(this.addr3)[transferFn]( - this.addr1.address, this.addr3.address, this.tokenIds - ); - // prettier-ignore - await expect( - this.erc721aBatchTransferable - .connect(this.addr1)[transferFn]( - this.addr3.address, this.addr1.address, this.tokenIds - ) - ).to.be.revertedWith('TransferCallerNotOwnerNorApproved'); - }); - - it('self-approval is cleared on batch transfers', async function () { - for (let i = 0; i < this.tokenIds.length; i++) { - const tokenId = this.tokenIds[i]; - await this.erc721aBatchTransferable.connect(this.addr1).approve(this.addr1.address, tokenId); - expect(await this.erc721aBatchTransferable.getApproved(tokenId)).to.equal(this.addr1.address); - } - - // prettier-ignore - await this.erc721aBatchTransferable - .connect(this.addr1)[transferFn]( - this.addr1.address, this.addr2.address, this.tokenIds - ); - for (let i = 0; i < this.tokenIds.length; i++) { - const tokenId = this.tokenIds[i]; - expect(await this.erc721aBatchTransferable.getApproved(tokenId)).to.not.equal(this.addr1.address); - } - }); - - it('approval for all allows batch transfers', async function () { - await this.erc721aBatchTransferable.connect(this.addr1).setApprovalForAll(this.addr3.address, true); - - // prettier-ignore - await this.erc721aBatchTransferable - .connect(this.addr3)[transferFn]( - this.addr1.address, this.addr3.address, this.tokenIds - ); - }); - }); - }; - - context('successful transfers', function () { - context('batchTransferFrom', function (fn = 'batchTransferFrom') { - describe('to contract', function () { - testSuccessfulBatchTransfer(fn); - testUnsuccessfulBatchTransfer(fn); - testApproveBatchTransfer(fn); - }); - - describe('to EOA', function () { - testSuccessfulBatchTransfer(fn, false); - testUnsuccessfulBatchTransfer(fn, false); - testApproveBatchTransfer(fn, false); - }); - }); - context('safeBatchTransferFrom', function (fn = 'safeBatchTransferFrom(address,address,uint256[])') { - describe('to contract', function () { - testSuccessfulBatchTransfer(fn); - testUnsuccessfulBatchTransfer(fn); - testApproveBatchTransfer(fn); - }); - - describe('to EOA', function () { - testSuccessfulBatchTransfer(fn, false); - testUnsuccessfulBatchTransfer(fn, false); - testApproveBatchTransfer(fn, false); - }); - }); - - // Use to compare gas usage and verify expected behaviour with respect to normal transfers - context('batchTransferFromUnoptimized', function (fn = 'batchTransferFromUnoptimized') { - describe('to contract', function () { - testSuccessfulBatchTransfer(fn); - testUnsuccessfulBatchTransfer(fn); - testApproveBatchTransfer(fn); - }); - - describe('to EOA', function () { - testSuccessfulBatchTransfer(fn, false); - testUnsuccessfulBatchTransfer(fn, false); - testApproveBatchTransfer(fn, false); - }); - }); - }); - }); - }); - }; - -describe( - 'ERC721ABatchTransferable', - createTestSuite({ contract: 'ERC721ABatchTransferableMock', constructorArgs: ['Azuki', 'AZUKI'] }) -); From 43dc1f24e755f6cd9d4690a0ea98eeb645b41c01 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sat, 25 Mar 2023 18:54:04 +0100 Subject: [PATCH 57/74] changed _updateTokenId --- contracts/ERC721A.sol | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 2df8c8175..dc5946bd3 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -587,8 +587,11 @@ contract ERC721A is IERC721A { _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked) ); - // Updates next slot if necessary. - _updateTokenId(tokenId + 1, prevOwnershipPacked, prevOwnershipPacked); + // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . + if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { + // Updates next slot if necessary. + _updateTokenId(tokenId + 1, prevOwnershipPacked, _currentIndex); + } } // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean. @@ -728,23 +731,20 @@ contract ERC721A is IERC721A { * @dev Private function to handle updating ownership of `tokenId`. Used in `_batchTransferFrom`. * * `tokenId` - Token ID to initialize. - * `lastOwnershipPacked` - Slot of `tokenId - 1` * `prevOwnershipPacked` - Last initialized slot before `tokenId` + * `currentIndex` - Value of `_currentIndex`, cached in batch operations */ function _updateTokenId( uint256 tokenId, - uint256 lastOwnershipPacked, - uint256 prevOwnershipPacked + uint256 prevOwnershipPacked, + uint256 currentIndex ) private { - // If the next slot may not have been initialized (i.e. `nextInitialized == false`). - if (lastOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { - // If the next slot's address is zero and not burned (i.e. packed value is zero). - if (_packedOwnerships[tokenId] == 0) { - // If the next slot is within bounds. - if (tokenId != _currentIndex) { - // Initialize the next slot to maintain correctness for `ownerOf(tokenId)`. - _packedOwnerships[tokenId] = prevOwnershipPacked; - } + // If the next slot's address is zero and not burned (i.e. packed value is zero). + if (_packedOwnerships[tokenId] == 0) { + // If the next slot is within bounds. + if (tokenId != currentIndex) { + // Initialize the next slot to maintain correctness for `ownerOf(tokenId)`. + _packedOwnerships[tokenId] = prevOwnershipPacked; } } } @@ -1022,8 +1022,11 @@ contract ERC721A is IERC721A { (_BITMASK_BURNED | _BITMASK_NEXT_INITIALIZED) | _nextExtraData(from, address(0), prevOwnershipPacked) ); - // Updates next slot if necessary. - _updateTokenId(tokenId + 1, prevOwnershipPacked, prevOwnershipPacked); + // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . + if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { + // Updates next slot if necessary. + _updateTokenId(tokenId + 1, prevOwnershipPacked, _currentIndex); + } } emit Transfer(from, address(0), tokenId); @@ -1039,7 +1042,7 @@ contract ERC721A is IERC721A { * @dev Equivalent to `_batchBurn(tokenIds, false)`. */ function _batchBurn(uint256[] memory tokenIds) internal virtual { - _batchBurn(tokenIds, false); + _batchBurn(_msgSenderERC721A(), tokenIds, false); } /** @@ -1125,9 +1128,7 @@ contract ERC721A is IERC721A { (block.timestamp << _BITPOS_START_TIMESTAMP) | uint256(uint160(tokenOwner)); - // If the slot after the mini batch is neither out of bounds, nor initialized. - if (currTokenId != stop) - if (_packedOwnerships[currTokenId] == 0) _packedOwnerships[currTokenId] = prevOwnershipPacked; + _updateTokenId(currTokenId, prevOwnershipPacked, stop); // Update the address data in ERC721A's storage. // From 6e0401581be8f69ebae9adfe52245338159e2c0a Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sat, 25 Mar 2023 18:59:09 +0100 Subject: [PATCH 58/74] fixed mock --- contracts/mocks/ERC721ABatchBurnableMock.sol | 44 +++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/contracts/mocks/ERC721ABatchBurnableMock.sol b/contracts/mocks/ERC721ABatchBurnableMock.sol index da9c08c54..bc906e96f 100644 --- a/contracts/mocks/ERC721ABatchBurnableMock.sol +++ b/contracts/mocks/ERC721ABatchBurnableMock.sol @@ -34,33 +34,37 @@ contract ERC721ABatchBurnableMock is ERC721A, DirectBurnBitSetterHelper { return _numberBurned(owner); } - function bulkBurn(address burner, uint256[] memory tokenIds, bool approvalCheck) public { + function bulkBurn( + address burner, + uint256[] memory tokenIds, + bool approvalCheck + ) public { _batchBurn(burner, tokenIds, approvalCheck); } -} + function initializeOwnershipAt(uint256 index) public { + _initializeOwnershipAt(index); } - } - } - function batchBurnUnoptimized(uint256[] memory tokenIds) public { - tokenId = tokenIds[i]; - burn(tokenId); - for (uint256 i; i < tokenIds.length; ++i) { - uint256 tokenId; - unchecked { + function _extraData( + address, + address, + uint24 previousExtraData + ) internal view virtual override returns (uint24) { + return previousExtraData; } - _setExtraDataAt(index, extraData); - function setExtraDataAt(uint256 index, uint24 extraData) public { + function setExtraDataAt(uint256 index, uint24 extraData) public { + _setExtraDataAt(index, extraData); } - return previousExtraData; - ) internal view virtual override returns (uint24) { - uint24 previousExtraData - address, - address, - function _extraData( + function batchBurnUnoptimized(uint256[] memory tokenIds) public { + unchecked { + uint256 tokenId; + for (uint256 i; i < tokenIds.length; ++i) { + tokenId = tokenIds[i]; + _burn(tokenId); + } + } } - function initializeOwnershipAt(uint256 index) public { - _initializeOwnershipAt(index); +} From 172387bc827963671eb4606016331ae0999efb4f Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sat, 25 Mar 2023 19:34:20 +0100 Subject: [PATCH 59/74] fixed extension and mock --- contracts/extensions/ERC721ABatchBurnable.sol | 2 +- contracts/mocks/ERC721ABatchBurnableMock.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/extensions/ERC721ABatchBurnable.sol b/contracts/extensions/ERC721ABatchBurnable.sol index 93299e99e..3c5012745 100644 --- a/contracts/extensions/ERC721ABatchBurnable.sol +++ b/contracts/extensions/ERC721ABatchBurnable.sol @@ -14,6 +14,6 @@ import './IERC721ABatchBurnable.sol'; */ abstract contract ERC721ABatchBurnable is ERC721ABurnable, IERC721ABatchBurnable { function batchBurn(uint256[] memory tokenIds) public virtual override { - _batchBurn(tokenIds, true); + _batchBurn(msg.sender, tokenIds, true); } } diff --git a/contracts/mocks/ERC721ABatchBurnableMock.sol b/contracts/mocks/ERC721ABatchBurnableMock.sol index bc906e96f..b821fd17d 100644 --- a/contracts/mocks/ERC721ABatchBurnableMock.sol +++ b/contracts/mocks/ERC721ABatchBurnableMock.sol @@ -4,10 +4,10 @@ pragma solidity ^0.8.4; -import '../ERC721A.sol'; +import '../extensions/ERC721ABatchBurnable.sol'; import './DirectBurnBitSetterHelper.sol'; -contract ERC721ABatchBurnableMock is ERC721A, DirectBurnBitSetterHelper { +contract ERC721ABatchBurnableMock is ERC721ABatchBurnable, DirectBurnBitSetterHelper { constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {} function exists(uint256 tokenId) public view returns (bool) { From a6c2823b28b6916e899e7c2bb9e12edbc52e6774 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sat, 25 Mar 2023 19:52:05 +0100 Subject: [PATCH 60/74] fixed tests and cleaned unused functions in mock --- contracts/mocks/ERC721ABatchBurnableMock.sol | 12 ---- test/extensions/ERC721ABatchBurnable.test.js | 71 +++----------------- 2 files changed, 8 insertions(+), 75 deletions(-) diff --git a/contracts/mocks/ERC721ABatchBurnableMock.sol b/contracts/mocks/ERC721ABatchBurnableMock.sol index b821fd17d..0e87b67c6 100644 --- a/contracts/mocks/ERC721ABatchBurnableMock.sol +++ b/contracts/mocks/ERC721ABatchBurnableMock.sol @@ -46,18 +46,6 @@ contract ERC721ABatchBurnableMock is ERC721ABatchBurnable, DirectBurnBitSetterHe _initializeOwnershipAt(index); } - function _extraData( - address, - address, - uint24 previousExtraData - ) internal view virtual override returns (uint24) { - return previousExtraData; - } - - function setExtraDataAt(uint256 index, uint24 extraData) public { - _setExtraDataAt(index, extraData); - } - function batchBurnUnoptimized(uint256[] memory tokenIds) public { unchecked { uint256 tokenId; diff --git a/test/extensions/ERC721ABatchBurnable.test.js b/test/extensions/ERC721ABatchBurnable.test.js index 73ca794fa..42c77d9ab 100644 --- a/test/extensions/ERC721ABatchBurnable.test.js +++ b/test/extensions/ERC721ABatchBurnable.test.js @@ -146,7 +146,7 @@ const createTestSuite = ({ contract, constructorArgs }) => await expect(query).to.be.revertedWith('OwnerQueryForNonexistentToken'); }); - it('cannot burn tokens with different owners', async function () { + it('can burn tokens with different owners', async function () { const tokenIdsToBurn = [this.notBurnedTokenId1, this.notBurnedTokenId2, this.notBurnedTokenId3]; await this.erc721aBatchBurnable.connect(this.addr1).setApprovalForAll(this.spender.address, true); @@ -154,20 +154,13 @@ const createTestSuite = ({ contract, constructorArgs }) => .connect(this.addr1) .transferFrom(this.addr1.address, this.spender.address, this.notBurnedTokenId2); - await expect( - this.erc721aBatchBurnable.connect(this.spender).batchBurn(tokenIdsToBurn.slice(0, 2)) - ).to.be.revertedWith('TransferFromIncorrectOwner'); - // Expect also consecutive ids to revert - await expect( - this.erc721aBatchBurnable.connect(this.spender).batchBurn(tokenIdsToBurn.slice(1)) - ).to.be.revertedWith('TransferFromIncorrectOwner'); - - await expect( - this.erc721aBatchBurnable.connect(this.spender).batchBurn([this.notBurnedTokenId1]) - ).to.not.be.reverted; - await expect( - this.erc721aBatchBurnable.connect(this.spender).batchBurn([this.notBurnedTokenId2]) - ).to.not.be.reverted; + const totalBurnedBefore = (await this.erc721aBatchBurnable.totalBurned()).toNumber(); + await this.erc721aBatchBurnable.connect(this.spender).batchBurn(tokenIdsToBurn); + + expect(await this.erc721aBatchBurnable.exists(this.notBurnedTokenId1)).to.be.false; + expect(await this.erc721aBatchBurnable.exists(this.notBurnedTokenId2)).to.be.false; + expect(await this.erc721aBatchBurnable.exists(this.notBurnedTokenId3)).to.be.false; + expect((await this.erc721aBatchBurnable.totalBurned()).toNumber() - totalBurnedBefore).to.equal(3); }); it('does not affect _totalMinted', async function () { @@ -213,54 +206,6 @@ const createTestSuite = ({ contract, constructorArgs }) => } }); - it('with tokens burned and cleared', async function () { - const initializedToken = 15; - - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal(ZERO_ADDRESS); - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(ZERO_ADDRESS); - - // Initialize token - await this.erc721aBatchBurnable.initializeOwnershipAt(initializedToken); - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal(ZERO_ADDRESS); - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(this.addr1.address); - - // Burn tokens - await this.erc721aBatchBurnable.connect(this.addr1).batchBurn([initializedToken - 1, initializedToken]); - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( - this.addr1.address - ); - - // Initialized tokens in a consecutive burn are cleared - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(ZERO_ADDRESS); - }); - - it('with tokens burned and updated', async function () { - const initializedToken = 15; - const extraData = 123; - - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal(ZERO_ADDRESS); - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(ZERO_ADDRESS); - - // Initialize token - await this.erc721aBatchBurnable.initializeOwnershipAt(initializedToken); - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal(ZERO_ADDRESS); - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(this.addr1.address); - - // Set extra data - await this.erc721aBatchBurnable.setExtraDataAt(initializedToken, extraData); - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[3]).to.be.equal(extraData); - - // Burn tokens - await this.erc721aBatchBurnable.connect(this.addr1).batchBurn([initializedToken - 1, initializedToken]); - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( - this.addr1.address - ); - - // Initialized tokens in a consecutive burn are updated when nextData is not 0 - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(this.addr1.address); - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[3]).to.be.equal(extraData); - }); - it('with token before previously burnt token transferred and burned', async function () { await this.erc721aBatchBurnable .connect(this.addr1) From 4d1e7dd86d0e3da0f43846eabc79dec731ae8cb3 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sat, 25 Mar 2023 20:09:51 +0100 Subject: [PATCH 61/74] removed _updateTokenId --- contracts/ERC721A.sol | 48 +++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index dc5946bd3..ad492b1b9 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -589,8 +589,15 @@ contract ERC721A is IERC721A { // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { - // Updates next slot if necessary. - _updateTokenId(tokenId + 1, prevOwnershipPacked, _currentIndex); + uint256 nextTokenId = tokenId + 1; + // If the next slot's address is zero and not burned (i.e. packed value is zero). + if (_packedOwnerships[nextTokenId] == 0) { + // If the next slot is within bounds. + if (nextTokenId != _currentIndex) { + // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. + _packedOwnerships[nextTokenId] = prevOwnershipPacked; + } + } } } @@ -727,28 +734,6 @@ contract ERC721A is IERC721A { } } - /** - * @dev Private function to handle updating ownership of `tokenId`. Used in `_batchTransferFrom`. - * - * `tokenId` - Token ID to initialize. - * `prevOwnershipPacked` - Last initialized slot before `tokenId` - * `currentIndex` - Value of `_currentIndex`, cached in batch operations - */ - function _updateTokenId( - uint256 tokenId, - uint256 prevOwnershipPacked, - uint256 currentIndex - ) private { - // If the next slot's address is zero and not burned (i.e. packed value is zero). - if (_packedOwnerships[tokenId] == 0) { - // If the next slot is within bounds. - if (tokenId != currentIndex) { - // Initialize the next slot to maintain correctness for `ownerOf(tokenId)`. - _packedOwnerships[tokenId] = prevOwnershipPacked; - } - } - } - // ============================================================= // MINT OPERATIONS // ============================================================= @@ -1024,8 +1009,15 @@ contract ERC721A is IERC721A { // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { - // Updates next slot if necessary. - _updateTokenId(tokenId + 1, prevOwnershipPacked, _currentIndex); + uint256 nextTokenId = tokenId + 1; + // If the next slot's address is zero and not burned (i.e. packed value is zero). + if (_packedOwnerships[nextTokenId] == 0) { + // If the next slot is within bounds. + if (nextTokenId != _currentIndex) { + // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. + _packedOwnerships[nextTokenId] = prevOwnershipPacked; + } + } } } @@ -1128,7 +1120,9 @@ contract ERC721A is IERC721A { (block.timestamp << _BITPOS_START_TIMESTAMP) | uint256(uint160(tokenOwner)); - _updateTokenId(currTokenId, prevOwnershipPacked, stop); + // If the slot after the mini batch is neither out of bounds, nor initialized. + if (currTokenId != stop) + if (_packedOwnerships[currTokenId] == 0) _packedOwnerships[currTokenId] = prevOwnershipPacked; // Update the address data in ERC721A's storage. // From c3438c00c266f6c235b6cfd48fd3a783382f668f Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sat, 25 Mar 2023 20:23:35 +0100 Subject: [PATCH 62/74] minor gas optimizations --- contracts/ERC721A.sol | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index ad492b1b9..c5860eebf 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1065,8 +1065,13 @@ contract ERC721A is IERC721A { // For checking if the `tokenIds` are strictly ascending. uint256 prevTokenId; + uint256 tokenId; + uint256 currTokenId; + uint256 prevOwnershipPacked; + address tokenOwner; + bool mayBurn; for (uint256 i; i != n; ) { - uint256 tokenId = tokenIds[i]; + tokenId = tokenIds[i]; // Revert `tokenId` is out of bounds. if (_or(tokenId < _startTokenId(), stop <= tokenId)) revert OwnerQueryForNonexistentToken(); @@ -1075,8 +1080,6 @@ contract ERC721A is IERC721A { if (i != 0) if (tokenId <= prevTokenId) revert TokenIdsNotStrictlyAscending(); - // The initialized packed ownership slot's value. - uint256 prevOwnershipPacked; // Scan backwards for an initialized packed ownership slot. // ERC721A's invariant guarantees that there will always be an initialized slot as long as // the start of the backwards scan falls within `[_startTokenId() .. _nextTokenId())`. @@ -1086,13 +1089,13 @@ contract ERC721A is IERC721A { if (prevOwnershipPacked & _BITMASK_BURNED != 0) revert OwnerQueryForNonexistentToken(); // Unpack the `tokenOwner` from bits [0..159] of `prevOwnershipPacked`. - address tokenOwner = address(uint160(prevOwnershipPacked)); + tokenOwner = address(uint160(prevOwnershipPacked)); - // Check if the burner is either the owner or an approved operator for all the - bool mayBurn = !approvalCheck || tokenOwner == burner || isApprovedForAll(tokenOwner, burner); + // Check if the burner is either the owner or an approved operator for all tokens + mayBurn = !approvalCheck || tokenOwner == burner || isApprovedForAll(tokenOwner, burner); + currTokenId = tokenId; uint256 offset; - uint256 currTokenId = tokenId; do { // Revert if the burner is not authorized to burn the token. if (!mayBurn) From 424c82e33e1d50de7eaf0475e1694b0c9e1547fa Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sat, 25 Mar 2023 21:08:48 +0100 Subject: [PATCH 63/74] comment --- contracts/ERC721A.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index c5860eebf..d5d9a879f 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1031,7 +1031,7 @@ contract ERC721A is IERC721A { } /** - * @dev Equivalent to `_batchBurn(tokenIds, false)`. + * @dev Equivalent to `_batchBurn(msg.sender, tokenIds, false)`. */ function _batchBurn(uint256[] memory tokenIds) internal virtual { _batchBurn(_msgSenderERC721A(), tokenIds, false); From d4a2442e73c0f9f972ac30e1762ad179da7074f5 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sat, 25 Mar 2023 21:11:59 +0100 Subject: [PATCH 64/74] optimize: avoid potential double read from storage --- contracts/ERC721A.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index d5d9a879f..4f0bf0a9b 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1096,6 +1096,7 @@ contract ERC721A is IERC721A { currTokenId = tokenId; uint256 offset; + uint256 lastPackedOwnedship; do { // Revert if the burner is not authorized to burn the token. if (!mayBurn) @@ -1114,7 +1115,7 @@ contract ERC721A is IERC721A { // Token ID is sequential. tokenIds[i + offset] == currTokenId && // The packed ownership slot is not initialized. - _packedOwnerships[currTokenId] == 0 + (lastPackedOwnedship = _packedOwnerships[currTokenId]) == 0 ); // Update the packed ownership for `tokenId` in ERC721A's storage. @@ -1125,7 +1126,8 @@ contract ERC721A is IERC721A { // If the slot after the mini batch is neither out of bounds, nor initialized. if (currTokenId != stop) - if (_packedOwnerships[currTokenId] == 0) _packedOwnerships[currTokenId] = prevOwnershipPacked; + if (lastPackedOwnedship != 0 || _packedOwnerships[currTokenId] == 0) + _packedOwnerships[currTokenId] = prevOwnershipPacked; // Update the address data in ERC721A's storage. // From 6c51c77250fa2a9d8fd0a8f60ef5468e7f666a6a Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sat, 25 Mar 2023 21:16:30 +0100 Subject: [PATCH 65/74] removed bulkBurn from mock --- contracts/mocks/ERC721ABatchBurnableMock.sol | 8 -------- 1 file changed, 8 deletions(-) diff --git a/contracts/mocks/ERC721ABatchBurnableMock.sol b/contracts/mocks/ERC721ABatchBurnableMock.sol index 0e87b67c6..058355ab3 100644 --- a/contracts/mocks/ERC721ABatchBurnableMock.sol +++ b/contracts/mocks/ERC721ABatchBurnableMock.sol @@ -34,14 +34,6 @@ contract ERC721ABatchBurnableMock is ERC721ABatchBurnable, DirectBurnBitSetterHe return _numberBurned(owner); } - function bulkBurn( - address burner, - uint256[] memory tokenIds, - bool approvalCheck - ) public { - _batchBurn(burner, tokenIds, approvalCheck); - } - function initializeOwnershipAt(uint256 index) public { _initializeOwnershipAt(index); } From e28b9980446fbc8e83e99347ad62264a669b88fd Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 26 Mar 2023 15:10:09 +0200 Subject: [PATCH 66/74] optimization: reset _packedOwnerships for initialized sequential IDs --- contracts/ERC721A.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 4f0bf0a9b..e6eccb550 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1096,7 +1096,6 @@ contract ERC721A is IERC721A { currTokenId = tokenId; uint256 offset; - uint256 lastPackedOwnedship; do { // Revert if the burner is not authorized to burn the token. if (!mayBurn) @@ -1115,7 +1114,9 @@ contract ERC721A is IERC721A { // Token ID is sequential. tokenIds[i + offset] == currTokenId && // The packed ownership slot is not initialized. - (lastPackedOwnedship = _packedOwnerships[currTokenId]) == 0 + (_packedOwnerships[currTokenId] == 0 || + // or if initialized, it is set to 0. + (_packedOwnerships[currTokenId] = 0) == 0) ); // Update the packed ownership for `tokenId` in ERC721A's storage. @@ -1126,8 +1127,7 @@ contract ERC721A is IERC721A { // If the slot after the mini batch is neither out of bounds, nor initialized. if (currTokenId != stop) - if (lastPackedOwnedship != 0 || _packedOwnerships[currTokenId] == 0) - _packedOwnerships[currTokenId] = prevOwnershipPacked; + if (_packedOwnerships[currTokenId] == 0) _packedOwnerships[currTokenId] = prevOwnershipPacked; // Update the address data in ERC721A's storage. // From 7e36bbba509537bfb44435eeb3c089175fe7f59b Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 26 Mar 2023 15:10:37 +0200 Subject: [PATCH 67/74] added tests for sequential ID clearing --- test/extensions/ERC721ABatchBurnable.test.js | 29 ++++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/test/extensions/ERC721ABatchBurnable.test.js b/test/extensions/ERC721ABatchBurnable.test.js index 42c77d9ab..c5ad35e09 100644 --- a/test/extensions/ERC721ABatchBurnable.test.js +++ b/test/extensions/ERC721ABatchBurnable.test.js @@ -34,12 +34,13 @@ const createTestSuite = ({ contract, constructorArgs }) => this.uninitializedToken = 13; await this.erc721aBatchBurnable['safeMint(address,uint256)'](this.addr1.address, this.numTestTokens); + // Manually initialize token IDs + await this.erc721aBatchBurnable.initializeOwnershipAt(3); + await this.erc721aBatchBurnable.initializeOwnershipAt(this.initializedToken); + await this.erc721aBatchBurnable .connect(this.addr1) .batchBurn([...this.burnedTokenIds1, ...this.burnedTokenIds2]); - - // Manually initialize `this.initializedToken` - await this.erc721aBatchBurnable.initializeOwnershipAt(this.initializedToken); }); context('totalSupply()', function () { @@ -206,6 +207,28 @@ const createTestSuite = ({ contract, constructorArgs }) => } }); + it('with tokens burned and cleared', async function () { + const initializedToken = 15; + + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal(ZERO_ADDRESS); + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(ZERO_ADDRESS); + + // Initialize token + await this.erc721aBatchBurnable.initializeOwnershipAt(initializedToken); + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal(ZERO_ADDRESS); + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(this.addr1.address); + + // Burn tokens + await this.erc721aBatchBurnable.connect(this.addr1).batchBurn([initializedToken - 1, initializedToken]); + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( + this.addr1.address + ); + + // Initialized tokens in a consecutive burn are cleared + expect((await this.erc721aBatchBurnable.getOwnershipAt(3))[0]).to.be.equal(ZERO_ADDRESS); + expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(ZERO_ADDRESS); + }); + it('with token before previously burnt token transferred and burned', async function () { await this.erc721aBatchBurnable .connect(this.addr1) From 47504827078daa2abfea1468ae6c93a516a5a5ce Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 26 Mar 2023 15:56:34 +0200 Subject: [PATCH 68/74] added test for tokenIds in strictly ascending order --- test/extensions/ERC721ABatchBurnable.test.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/extensions/ERC721ABatchBurnable.test.js b/test/extensions/ERC721ABatchBurnable.test.js index c5ad35e09..d179afc6f 100644 --- a/test/extensions/ERC721ABatchBurnable.test.js +++ b/test/extensions/ERC721ABatchBurnable.test.js @@ -104,6 +104,13 @@ const createTestSuite = ({ contract, constructorArgs }) => await expect(query).to.be.revertedWith('OwnerQueryForNonexistentToken'); }); + it('can only burn tokenIds when provided in ascending order', async function () { + const query = this.erc721aBatchBurnable + .connect(this.addr1) + .batchBurn([this.notBurnedTokenId3, this.notBurnedTokenId2, this.notBurnedTokenId1]); + await expect(query).to.be.revertedWith('TokenIdsNotStrictlyAscending'); + }); + it('cannot burn a burned token', async function () { const query = this.erc721aBatchBurnable.connect(this.addr1).batchBurn(this.burnedTokenIds1); await expect(query).to.be.revertedWith('OwnerQueryForNonexistentToken'); From 2b2aafca630195ecfe816b9e5dc51b1b84d49d68 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 26 Mar 2023 16:28:44 +0200 Subject: [PATCH 69/74] comment --- contracts/ERC721A.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index e6eccb550..126359801 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1039,7 +1039,7 @@ contract ERC721A is IERC721A { /** * @dev Destroys `tokenIds`. - * The approval is cleared when the token is burned. + * Approvals are not cleared when tokenIds are burned. * * Requirements: * From 229bea6b887a2515c393ea386a97477d8df52cd7 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 26 Mar 2023 17:22:27 +0200 Subject: [PATCH 70/74] optimize: keep track of prevTokenOwner to bypass duplicated logic --- contracts/ERC721A.sol | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 126359801..55ee3e0d9 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1068,6 +1068,7 @@ contract ERC721A is IERC721A { uint256 tokenId; uint256 currTokenId; uint256 prevOwnershipPacked; + address prevTokenOwner; address tokenOwner; bool mayBurn; for (uint256 i; i != n; ) { @@ -1091,8 +1092,13 @@ contract ERC721A is IERC721A { // Unpack the `tokenOwner` from bits [0..159] of `prevOwnershipPacked`. tokenOwner = address(uint160(prevOwnershipPacked)); - // Check if the burner is either the owner or an approved operator for all tokens - mayBurn = !approvalCheck || tokenOwner == burner || isApprovedForAll(tokenOwner, burner); + if (tokenOwner != prevTokenOwner) { + // Update `prevTokenOwner`. + prevTokenOwner = tokenOwner; + + // Check if the burner is either the owner or an approved operator for all tokens + mayBurn = !approvalCheck || tokenOwner == burner || isApprovedForAll(tokenOwner, burner); + } currTokenId = tokenId; uint256 offset; From c72698854db6e998d82b263f7ddd2ce871294f26 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 26 Mar 2023 19:11:59 +0200 Subject: [PATCH 71/74] revert: resetting _packedOwnerships in initialized sequential IDs --- contracts/ERC721A.sol | 4 +- test/extensions/ERC721ABatchBurnable.test.js | 71 +++++++++++++------- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 55ee3e0d9..a37c9261b 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1120,9 +1120,7 @@ contract ERC721A is IERC721A { // Token ID is sequential. tokenIds[i + offset] == currTokenId && // The packed ownership slot is not initialized. - (_packedOwnerships[currTokenId] == 0 || - // or if initialized, it is set to 0. - (_packedOwnerships[currTokenId] = 0) == 0) + (_packedOwnerships[currTokenId] == 0) ); // Update the packed ownership for `tokenId` in ERC721A's storage. diff --git a/test/extensions/ERC721ABatchBurnable.test.js b/test/extensions/ERC721ABatchBurnable.test.js index d179afc6f..8c3fbe975 100644 --- a/test/extensions/ERC721ABatchBurnable.test.js +++ b/test/extensions/ERC721ABatchBurnable.test.js @@ -23,6 +23,7 @@ const createTestSuite = ({ contract, constructorArgs }) => this.addr2 = addr2; this.spender = spender; this.numTestTokens = 20; + this.totalTokens = 40; this.totalBurned = 6; this.burnedTokenIds1 = [2, 3, 4]; this.burnedTokenIds2 = [7, 9, 10]; @@ -34,6 +35,7 @@ const createTestSuite = ({ contract, constructorArgs }) => this.uninitializedToken = 13; await this.erc721aBatchBurnable['safeMint(address,uint256)'](this.addr1.address, this.numTestTokens); + await this.erc721aBatchBurnable['safeMint(address,uint256)'](this.addr2.address, this.numTestTokens); // Manually initialize token IDs await this.erc721aBatchBurnable.initializeOwnershipAt(3); await this.erc721aBatchBurnable.initializeOwnershipAt(this.initializedToken); @@ -45,7 +47,7 @@ const createTestSuite = ({ contract, constructorArgs }) => context('totalSupply()', function () { it('has the expected value', async function () { - expect(await this.erc721aBatchBurnable.totalSupply()).to.equal(this.numTestTokens - this.totalBurned); + expect(await this.erc721aBatchBurnable.totalSupply()).to.equal(this.totalTokens - this.totalBurned); }); it('is reduced by burns', async function () { @@ -94,13 +96,13 @@ const createTestSuite = ({ contract, constructorArgs }) => .batchBurn(offsetted(this.notBurnedTokenId3, this.notBurnedTokenId4)); expect(await this.erc721aBatchBurnable.exists(this.notBurnedTokenId3)).to.be.false; expect(await this.erc721aBatchBurnable.exists(this.notBurnedTokenId4)).to.be.false; - expect(await this.erc721aBatchBurnable.exists(this.numTestTokens)).to.be.false; + expect(await this.erc721aBatchBurnable.exists(this.totalTokens)).to.be.false; }); it('cannot burn a non-existing token', async function () { const query = this.erc721aBatchBurnable .connect(this.addr1) - .batchBurn([this.notBurnedTokenId4, this.numTestTokens]); + .batchBurn([this.notBurnedTokenId4, this.totalTokens]); await expect(query).to.be.revertedWith('OwnerQueryForNonexistentToken'); }); @@ -128,6 +130,17 @@ const createTestSuite = ({ contract, constructorArgs }) => await expect(query).to.be.revertedWith('TransferCallerNotOwnerNorApproved'); }); + it('cannot burn sequential ID with wrong owner', async function () { + const tokenIdsToBurn = [this.notBurnedTokenId2, this.notBurnedTokenId3]; + + await this.erc721aBatchBurnable.connect(this.addr1).approve(this.spender.address, tokenIdsToBurn[0]); + + const query1 = this.erc721aBatchBurnable.connect(this.spender).batchBurn(tokenIdsToBurn); + await expect(query1).to.be.revertedWith('TransferCallerNotOwnerNorApproved'); + const query2 = this.erc721aBatchBurnable.connect(this.addr1).batchBurn([19, 20]); + await expect(query2).to.be.revertedWith('TransferCallerNotOwnerNorApproved'); + }); + it('spender can burn with specific approved tokenId', async function () { const tokenIdsToBurn = [this.notBurnedTokenId1, this.notBurnedTokenId2]; @@ -162,6 +175,11 @@ const createTestSuite = ({ contract, constructorArgs }) => .connect(this.addr1) .transferFrom(this.addr1.address, this.spender.address, this.notBurnedTokenId2); + await this.erc721aBatchBurnable + .connect(this.addr1) + .transferFrom(this.addr1.address, this.addr2.address, this.notBurnedTokenId3); + await this.erc721aBatchBurnable.connect(this.addr2).approve(this.spender.address, this.notBurnedTokenId3); + const totalBurnedBefore = (await this.erc721aBatchBurnable.totalBurned()).toNumber(); await this.erc721aBatchBurnable.connect(this.spender).batchBurn(tokenIdsToBurn); @@ -174,7 +192,7 @@ const createTestSuite = ({ contract, constructorArgs }) => it('does not affect _totalMinted', async function () { const tokenIdsToBurn = [this.notBurnedTokenId1, this.notBurnedTokenId2]; const totalMintedBefore = await this.erc721aBatchBurnable.totalMinted(); - expect(totalMintedBefore).to.equal(this.numTestTokens); + expect(totalMintedBefore).to.equal(this.totalTokens); await this.erc721aBatchBurnable.connect(this.addr1).batchBurn(tokenIdsToBurn); expect(await this.erc721aBatchBurnable.totalMinted()).to.equal(totalMintedBefore); }); @@ -206,7 +224,7 @@ const createTestSuite = ({ contract, constructorArgs }) => await this.erc721aBatchBurnable.connect(this.addr1).batchBurn([this.notBurnedTokenId1]); for (let i = 0; i < this.numTestTokens; ++i) { - const initializedTokens = [0, 2, 5, 7, 8, 9, 11, 12, this.notBurnedTokenId1]; + const initializedTokens = [0, 2, 3, 5, 7, 8, 9, 11, 12, this.notBurnedTokenId1]; expect((await this.erc721aBatchBurnable.getOwnershipAt(i))[0]).to.be.equal( initializedTokens.includes(i) ? this.addr1.address : ZERO_ADDRESS @@ -214,27 +232,30 @@ const createTestSuite = ({ contract, constructorArgs }) => } }); - it('with tokens burned and cleared', async function () { - const initializedToken = 15; + // it('with tokens burned and cleared', async function () { + // const initializedToken = 15; - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal(ZERO_ADDRESS); - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(ZERO_ADDRESS); + // expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]) + // .to.be.equal(ZERO_ADDRESS); + // expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(ZERO_ADDRESS); - // Initialize token - await this.erc721aBatchBurnable.initializeOwnershipAt(initializedToken); - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal(ZERO_ADDRESS); - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(this.addr1.address); + // // Initialize token + // await this.erc721aBatchBurnable.initializeOwnershipAt(initializedToken); + // expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]) + // .to.be.equal(ZERO_ADDRESS); + // expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]) + // .to.be.equal(this.addr1.address); - // Burn tokens - await this.erc721aBatchBurnable.connect(this.addr1).batchBurn([initializedToken - 1, initializedToken]); - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( - this.addr1.address - ); + // // Burn tokens + // await this.erc721aBatchBurnable.connect(this.addr1).batchBurn([initializedToken - 1, initializedToken]); + // expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken - 1))[0]).to.be.equal( + // this.addr1.address + // ); - // Initialized tokens in a consecutive burn are cleared - expect((await this.erc721aBatchBurnable.getOwnershipAt(3))[0]).to.be.equal(ZERO_ADDRESS); - expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(ZERO_ADDRESS); - }); + // // Initialized tokens in a consecutive burn are cleared + // expect((await this.erc721aBatchBurnable.getOwnershipAt(3))[0]).to.be.equal(ZERO_ADDRESS); + // expect((await this.erc721aBatchBurnable.getOwnershipAt(initializedToken))[0]).to.be.equal(ZERO_ADDRESS); + // }); it('with token before previously burnt token transferred and burned', async function () { await this.erc721aBatchBurnable @@ -283,11 +304,11 @@ const createTestSuite = ({ contract, constructorArgs }) => }); it('with last token burned', async function () { - await expect(this.erc721aBatchBurnable.ownerOf(offsetted(this.numTestTokens))).to.be.revertedWith( + await expect(this.erc721aBatchBurnable.ownerOf(offsetted(this.totalTokens))).to.be.revertedWith( 'OwnerQueryForNonexistentToken' ); - await this.erc721aBatchBurnable.connect(this.addr1).batchBurn([offsetted(this.numTestTokens - 1)]); - await expect(this.erc721aBatchBurnable.ownerOf(offsetted(this.numTestTokens - 1))).to.be.revertedWith( + await this.erc721aBatchBurnable.connect(this.addr2).batchBurn([offsetted(this.totalTokens - 1)]); + await expect(this.erc721aBatchBurnable.ownerOf(offsetted(this.totalTokens - 1))).to.be.revertedWith( 'OwnerQueryForNonexistentToken' ); }); From 90dcfe3c3261b8eb49c345dcb0d3ce09bf2d1aaa Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 26 Mar 2023 19:13:46 +0200 Subject: [PATCH 72/74] cleanup --- contracts/ERC721A.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index a37c9261b..5ae323854 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1120,7 +1120,7 @@ contract ERC721A is IERC721A { // Token ID is sequential. tokenIds[i + offset] == currTokenId && // The packed ownership slot is not initialized. - (_packedOwnerships[currTokenId] == 0) + _packedOwnerships[currTokenId] == 0 ); // Update the packed ownership for `tokenId` in ERC721A's storage. From 6f07d87e9f242b7a110c0d004267a53fe3a0d68d Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 26 Mar 2023 20:19:09 +0200 Subject: [PATCH 73/74] optimize: avoid potential double read from storage --- contracts/ERC721A.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 5ae323854..2e7f38e42 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1069,6 +1069,7 @@ contract ERC721A is IERC721A { uint256 currTokenId; uint256 prevOwnershipPacked; address prevTokenOwner; + uint256 lastOwnershipPacked; address tokenOwner; bool mayBurn; for (uint256 i; i != n; ) { @@ -1120,7 +1121,7 @@ contract ERC721A is IERC721A { // Token ID is sequential. tokenIds[i + offset] == currTokenId && // The packed ownership slot is not initialized. - _packedOwnerships[currTokenId] == 0 + (lastOwnershipPacked = _packedOwnerships[currTokenId]) == 0 ); // Update the packed ownership for `tokenId` in ERC721A's storage. @@ -1131,7 +1132,9 @@ contract ERC721A is IERC721A { // If the slot after the mini batch is neither out of bounds, nor initialized. if (currTokenId != stop) - if (_packedOwnerships[currTokenId] == 0) _packedOwnerships[currTokenId] = prevOwnershipPacked; + if (lastOwnershipPacked == 0) + // If `lastOwnershipPacked == 0` we didn't break the loop due to an initialized slot. + if (_packedOwnerships[currTokenId] == 0) _packedOwnerships[currTokenId] = prevOwnershipPacked; // Update the address data in ERC721A's storage. // From 2cdff656d466cb65ed2db8ba9c2cd26e9ed52bcd Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 26 Mar 2023 23:10:06 +0200 Subject: [PATCH 74/74] optimize: removed unneeded exists() via getApproved --- contracts/ERC721A.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 2e7f38e42..da3e83a81 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1106,7 +1106,7 @@ contract ERC721A is IERC721A { do { // Revert if the burner is not authorized to burn the token. if (!mayBurn) - if (getApproved(currTokenId) != burner) revert TransferCallerNotOwnerNorApproved(); + if (_tokenApprovals[currTokenId].value != burner) revert TransferCallerNotOwnerNorApproved(); // Call the hook. _beforeTokenTransfers(tokenOwner, address(0), currTokenId, 1); // Emit the `Transfer` event for burn. @@ -1133,8 +1133,9 @@ contract ERC721A is IERC721A { // If the slot after the mini batch is neither out of bounds, nor initialized. if (currTokenId != stop) if (lastOwnershipPacked == 0) - // If `lastOwnershipPacked == 0` we didn't break the loop due to an initialized slot. - if (_packedOwnerships[currTokenId] == 0) _packedOwnerships[currTokenId] = prevOwnershipPacked; + if (_packedOwnerships[currTokenId] == 0) + // If `lastOwnershipPacked == 0` we didn't break the loop due to an initialized slot. + _packedOwnerships[currTokenId] = prevOwnershipPacked; // Update the address data in ERC721A's storage. //