Skip to content

Commit

Permalink
Update ERC721A.sol
Browse files Browse the repository at this point in the history
  • Loading branch information
Vectorized authored Jun 14, 2022
1 parent e4112a4 commit 79e91e2
Showing 1 changed file with 165 additions and 48 deletions.
213 changes: 165 additions & 48 deletions contracts/ERC721A.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ 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 maximum `quantity` that can be minted with `_mintERC2309`.
// This limit is to prevent overflows on the address data entries.
// For a limit of 5000, a total of 3.689e15 calls to `_mintERC2309`
// is required to cause an overflow, which is unrealistic.
uint256 private constant MAX_MINT_ERC2309_QUANTITY_LIMIT = 5000;

// The tokenId of the next token to be minted.
uint256 private _currentIndex;

Expand All @@ -77,6 +89,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.
Expand Down Expand Up @@ -163,7 +176,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;
}

Expand Down Expand Up @@ -239,6 +252,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);
}

/**
Expand All @@ -265,6 +279,18 @@ contract ERC721A is IERC721A {
return _unpackedOwnership(_packedOwnershipOf(tokenId));
}

/**
* @dev Packs ownership data into a single uint256.
*/
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))
}
}

/**
* @dev See {IERC721-ownerOf}.
*/
Expand Down Expand Up @@ -305,15 +331,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.
*/
Expand Down Expand Up @@ -432,6 +449,8 @@ contract ERC721A is IERC721A {
* {IERC721Receiver-onERC721Received}, which is called for each safe transfer.
* - `quantity` must be greater than 0.
*
* See {_mint}.
*
* Emits a {Transfer} event for each mint.
*/
function _safeMint(
Expand Down Expand Up @@ -468,7 +487,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);
Expand All @@ -489,31 +508,92 @@ contract ERC721A is IERC721A {
// - `startTimestamp` to the timestamp of minting.
// - `burned` to `false`.
// - `nextInitialized` to `quantity == 1`.
_packedOwnerships[startTokenId] =
_addressToUint256(to) |
(block.timestamp << BITPOS_START_TIMESTAMP) |
(_boolToUint256(quantity == 1) << BITPOS_NEXT_INITIALIZED);
_packedOwnerships[startTokenId] = _packOwnershipData(
to,
(_boolToUint256(quantity == 1) << BITPOS_NEXT_INITIALIZED) | _nextExtraData(address(0), to, 0)
);

uint256 offset;
uint256 offset = startTokenId;
uint256 end = quantity + startTokenId;
do {
emit Transfer(address(0), to, startTokenId + offset++);
} while (offset < quantity);
emit Transfer(address(0), to, offset++);
} while (offset < end);

_currentIndex = startTokenId + quantity;
}
_afterTokenTransfers(address(0), to, startTokenId, quantity);
}

/**
* @dev Mints `quantity` tokens and transfers them to `to`.
*
* This function is intended for efficient minting only during contract creation.
*
* It emits only one {ConsecutiveTransfer} as defined in
* [ERC2309](https://eips.ethereum.org/EIPS/eip-2309),
* instead of a sequence of {Transfer} event(s).
*
* Calling this function outside of contract creation WILL make your contract
* non-compliant with the ERC721 standard.
* For full ERC721 compliance, substituting ERC721 {Transfer} event(s) with the ERC2309
* {ConsecutiveTransfer} event is only permissible during contract creation.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - `quantity` must be greater than 0.
*
* Emits a {ConsecutiveTransfer} event.
*/
function _mintERC2309(address to, uint256 quantity) internal {
uint256 startTokenId = _currentIndex;
if (to == address(0)) revert MintToZeroAddress();
if (quantity == 0) revert MintZeroQuantity();
if (quantity > MAX_MINT_ERC2309_QUANTITY_LIMIT) revert MintERC2309QuantityExceedsLimit();

_beforeTokenTransfers(address(0), to, startTokenId, quantity);

// Overflows are unrealistic due to the above check for `quantity` to be below the limit.
unchecked {
// Updates:
// - `balance += quantity`.
// - `numberMinted += quantity`.
//
// We can directly add to the balance and number minted.
_packedAddressData[to] += quantity * ((1 << BITPOS_NUMBER_MINTED) | 1);

// Updates:
// - `address` to the owner.
// - `startTimestamp` to the timestamp of minting.
// - `burned` to `false`.
// - `nextInitialized` to `quantity == 1`.
_packedOwnerships[startTokenId] = _packOwnershipData(
to,
(_boolToUint256(quantity == 1) << BITPOS_NEXT_INITIALIZED) | _nextExtraData(address(0), to, 0)
);

emit ConsecutiveTransfer(startTokenId, startTokenId + quantity - 1, address(0), to);

_currentIndex = startTokenId + quantity;
}
_afterTokenTransfers(address(0), to, startTokenId, quantity);
}

/**
* @dev Zeroes out _tokenApprovals[tokenId]
* @dev Returns the variables required for approval check.
*/
function _deleteTokenApproval(uint256 tokenId) private {
mapping(uint256 => address) storage tokenApprovalPtr = _tokenApprovals;
function _getApproved(
uint256 tokenId,
address from,
address msgSender
) private view returns (bool isApprovedOrOwner, uint256 approvedAddressSlot, address approvedAddress) {
mapping(uint256 => address) storage tokenApprovalsPtr = _tokenApprovals;
assembly {
mstore(0x00, tokenId)
mstore(0x20, tokenApprovalPtr.slot)
let hash := keccak256(0, 0x40)
sstore(hash, 0)
mstore(0x20, tokenApprovalsPtr.slot)
approvedAddressSlot := keccak256(0x00, 0x40)
approvedAddress := sload(approvedAddressSlot)
isApprovedOrOwner := or(eq(msgSender, from), eq(msgSender, approvedAddress))
}
}

Expand All @@ -536,20 +616,21 @@ contract ERC721A is IERC721A {

if (address(uint160(prevOwnershipPacked)) != from) revert TransferFromIncorrectOwner();

address approvedAddress = _tokenApprovals[tokenId];
(bool isApprovedOrOwner, uint256 approvedAddressSlot, address approvedAddress) =
_getApproved(tokenId, from, _msgSenderERC721A());

bool isApprovedOrOwner = (_msgSenderERC721A() == from ||
isApprovedForAll(from, _msgSenderERC721A()) ||
approvedAddress == _msgSenderERC721A());
isApprovedOrOwner = isApprovedOrOwner || isApprovedForAll(from, _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) {
_deleteTokenApproval(tokenId);
assembly {
if approvedAddress {
sstore(approvedAddressSlot, 0)
}
}

// Underflow of the sender's balance is impossible because we check for
Expand All @@ -565,10 +646,10 @@ contract ERC721A is IERC721A {
// - `startTimestamp` to the timestamp of transfering.
// - `burned` to `false`.
// - `nextInitialized` to `true`.
_packedOwnerships[tokenId] =
_addressToUint256(to) |
(block.timestamp << BITPOS_START_TIMESTAMP) |
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) {
Expand Down Expand Up @@ -608,22 +689,23 @@ contract ERC721A is IERC721A {
function _burn(uint256 tokenId, bool approvalCheck) internal virtual {
uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId);

address from = address(uint160(prevOwnershipPacked));
address approvedAddress = _tokenApprovals[tokenId];
address from = address(uint160(prevOwnershipPacked & BITMASK_ADDRESS));

if (approvalCheck) {
bool isApprovedOrOwner = (_msgSenderERC721A() == from ||
isApprovedForAll(from, _msgSenderERC721A()) ||
approvedAddress == _msgSenderERC721A());
(bool isApprovedOrOwner, uint256 approvedAddressSlot, address approvedAddress) =
_getApproved(tokenId, from, _msgSenderERC721A());

if (approvalCheck) {
isApprovedOrOwner = isApprovedOrOwner || isApprovedForAll(from, _msgSenderERC721A());
if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved();
}

_beforeTokenTransfers(from, address(0), tokenId, 1);

// Clear approvals from the previous owner.
if (_addressToUint256(approvedAddress) != 0) {
_deleteTokenApproval(tokenId);
assembly {
if approvedAddress {
sstore(approvedAddressSlot, 0)
}
}

// Underflow of the sender's balance is impossible because we check for
Expand All @@ -643,11 +725,10 @@ contract ERC721A is IERC721A {
// - `startTimestamp` to the timestamp of burning.
// - `burned` to `true`.
// - `nextInitialized` to `true`.
_packedOwnerships[tokenId] =
_addressToUint256(from) |
(block.timestamp << BITPOS_START_TIMESTAMP) |
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) {
Expand Down Expand Up @@ -702,6 +783,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.
Expand Down

0 comments on commit 79e91e2

Please sign in to comment.