diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index ef6c563c8..089462087 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -56,6 +56,12 @@ contract ERC721A is IERC721A { // The bit mask of the `nextInitialized` bit in packed ownership. uint256 private constant BITMASK_NEXT_INITIALIZED = 1 << 225; + // The bit position of `extraData` in packed ownership. + uint256 private constant BITPOS_EXTRA_DATA = 232; + + // The mask of the lower 160 bits for addresses. + uint256 private constant BITMASK_ADDRESS = (1 << 160) - 1; + // The tokenId of the next token to be minted. uint256 private _currentIndex; @@ -77,6 +83,7 @@ contract ERC721A is IERC721A { // - [160..223] `startTimestamp` // - [224] `burned` // - [225] `nextInitialized` + // - [232..255] `extraData` mapping(uint256 => uint256) private _packedOwnerships; // Mapping owner address to address data. @@ -163,7 +170,7 @@ contract ERC721A is IERC721A { * @dev See {IERC721-balanceOf}. */ function balanceOf(address owner) public view override returns (uint256) { - if (_addressToUint256(owner) == 0) revert BalanceQueryForZeroAddress(); + if (owner == address(0)) revert BalanceQueryForZeroAddress(); return _packedAddressData[owner] & BITMASK_ADDRESS_DATA_ENTRY; } @@ -239,6 +246,7 @@ contract ERC721A is IERC721A { ownership.addr = address(uint160(packed)); ownership.startTimestamp = uint64(packed >> BITPOS_START_TIMESTAMP); ownership.burned = packed & BITMASK_BURNED != 0; + ownership.extraData = uint24(packed >> BITPOS_EXTRA_DATA); } /** @@ -270,6 +278,8 @@ contract ERC721A is IERC721A { */ function _packOwnershipData(address owner, uint256 flags) private view returns (uint256 value) { assembly { + // Mask `owner` to the lower 160 bits, in case the upper bits somehow aren't clean. + owner := and(owner, BITMASK_ADDRESS) // `owner | (block.timestamp << BITPOS_START_TIMESTAMP) | flags`. value := or(owner, or(shl(BITPOS_START_TIMESTAMP, timestamp()), flags)) } @@ -315,15 +325,6 @@ contract ERC721A is IERC721A { return ''; } - /** - * @dev Casts the address to uint256 without masking. - */ - function _addressToUint256(address value) private pure returns (uint256 result) { - assembly { - result := value - } - } - /** * @dev Casts the boolean to uint256 without branching. */ @@ -478,7 +479,7 @@ contract ERC721A is IERC721A { */ function _mint(address to, uint256 quantity) internal { uint256 startTokenId = _currentIndex; - if (_addressToUint256(to) == 0) revert MintToZeroAddress(); + if (to == address(0)) revert MintToZeroAddress(); if (quantity == 0) revert MintZeroQuantity(); _beforeTokenTransfers(address(0), to, startTokenId, quantity); @@ -501,7 +502,7 @@ contract ERC721A is IERC721A { // - `nextInitialized` to `quantity == 1`. _packedOwnerships[startTokenId] = _packOwnershipData( to, - _boolToUint256(quantity == 1) << BITPOS_NEXT_INITIALIZED + (_boolToUint256(quantity == 1) << BITPOS_NEXT_INITIALIZED) | _nextExtraData(address(0), to, 0) ); uint256 offset = startTokenId; @@ -541,12 +542,12 @@ contract ERC721A is IERC721A { approvedAddress == _msgSenderERC721A()); if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved(); - if (_addressToUint256(to) == 0) revert TransferToZeroAddress(); + if (to == address(0)) revert TransferToZeroAddress(); _beforeTokenTransfers(from, to, tokenId, 1); // Clear approvals from the previous owner. - if (_addressToUint256(approvedAddress) != 0) { + if (approvedAddress != address(0)) { delete _tokenApprovals[tokenId]; } @@ -563,7 +564,10 @@ contract ERC721A is IERC721A { // - `startTimestamp` to the timestamp of transfering. // - `burned` to `false`. // - `nextInitialized` to `true`. - _packedOwnerships[tokenId] = _packOwnershipData(to, BITMASK_NEXT_INITIALIZED); + _packedOwnerships[tokenId] = _packOwnershipData( + to, + 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) { @@ -617,7 +621,7 @@ contract ERC721A is IERC721A { _beforeTokenTransfers(from, address(0), tokenId, 1); // Clear approvals from the previous owner. - if (_addressToUint256(approvedAddress) != 0) { + if (approvedAddress != address(0)) { delete _tokenApprovals[tokenId]; } @@ -638,7 +642,10 @@ contract ERC721A is IERC721A { // - `startTimestamp` to the timestamp of burning. // - `burned` to `true`. // - `nextInitialized` to `true`. - _packedOwnerships[tokenId] = _packOwnershipData(from, BITMASK_BURNED | BITMASK_NEXT_INITIALIZED); + _packedOwnerships[tokenId] = _packOwnershipData( + from, + (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) { @@ -693,6 +700,42 @@ contract ERC721A is IERC721A { } } + /** + * @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 + ) internal view virtual returns (uint256) { + uint24 previousExtraData; + assembly { + previousExtraData := shr(BITPOS_EXTRA_DATA, prevOwnershipPacked) + } + return uint256(_extraData(from, to, previousExtraData)) << BITPOS_EXTRA_DATA; + } + + /** + * @dev Called during each token transfer to set the 24bit `extraData` field. + * Intended to be overridden by the cosumer contract. + * + * `previousExtraData` - the value of `extraData` before transfer. + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, `from`'s `tokenId` will be + * transferred to `to`. + * - When `from` is zero, `tokenId` will be minted for `to`. + * - When `to` is zero, `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) {} + /** * @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. diff --git a/contracts/IERC721A.sol b/contracts/IERC721A.sol index 6d0be7231..5f59d90b3 100644 --- a/contracts/IERC721A.sol +++ b/contracts/IERC721A.sol @@ -75,6 +75,8 @@ interface IERC721A { uint64 startTimestamp; // Whether the token has been burned. bool burned; + // Arbitrary data similar to `startTimestamp` that can be set through `_extraData`. + uint24 extraData; } /** diff --git a/contracts/extensions/ERC721AQueryable.sol b/contracts/extensions/ERC721AQueryable.sol index 2088d2d74..f4ac4440d 100644 --- a/contracts/extensions/ERC721AQueryable.sol +++ b/contracts/extensions/ERC721AQueryable.sol @@ -19,16 +19,19 @@ abstract contract ERC721AQueryable is ERC721A, IERC721AQueryable { * - `addr` = `address(0)` * - `startTimestamp` = `0` * - `burned` = `false` + * - `extraData` = `0` * * If the `tokenId` is burned: * - `addr` = `
` * - `startTimestamp` = `