Skip to content

Commit

Permalink
Tidy
Browse files Browse the repository at this point in the history
  • Loading branch information
Vectorized committed Aug 21, 2024
1 parent ba80d01 commit e24ee8c
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 112 deletions.
207 changes: 99 additions & 108 deletions contracts/ERC721A.sol
Original file line number Diff line number Diff line change
Expand Up @@ -713,14 +713,14 @@ contract ERC721A is IERC721A {
}

/**
* @dev Equivalent to `_batchTransferFrom(from, to, tokenIds, false)`.
* @dev Equivalent to `_batchTransferFrom(from, to, tokenIds)`.
*/
function _batchTransferFrom(
address from,
address to,
uint256[] memory tokenIds
) internal virtual {
_batchTransferFrom(from, to, tokenIds, false);
_batchTransferFrom(address(0), from, to, tokenIds);
}

/**
Expand All @@ -732,149 +732,129 @@ contract ERC721A is IERC721A {
* - `to` cannot be the zero address.
* - `tokenIds` tokens must be owned by `from`.
* - `tokenIds` must be strictly ascending.
* - If the caller is not `from`, it must be approved to move these tokens
* - If `by` is not `from`, it must be approved to move these tokens
* by either {approve} or {setApprovalForAll}.
*
* `by` is the address that to check token approval for.
* If token approval check is not needed, pass in `address(0)` for `by`.
*
* Emits a {Transfer} event for each transfer.
*/
function _batchTransferFrom(
address by,
address from,
address to,
uint256[] memory tokenIds,
bool approvalCheck
uint256[] memory tokenIds
) internal virtual {
// We can use unchecked as the length of `tokenIds` is bounded
// to a small number by the max block gas limit.
uint256 fromMasked = uint256(uint160(from));
uint256 toMasked = uint256(uint160(to));
// Disallow transfer to zero address.
if (toMasked == uint256(0)) _revert(TransferToZeroAddress.selector);
// Early return if `tokenIds` is empty.
if (tokenIds.length == uint256(0)) return;
// Whether we need to check the individual token approvals.
bool approvalCheck = by != address(0) && by != from && !isApprovedForAll(from, by);
// The next `tokenId` to be minted (i.e. `_nextTokenId()`).
uint256 end = _currentIndex;
// Pointer to start and end (exclusive) of `tokenIds`.
(uint256 i, uint256 e) = _mdata(tokenIds);
// Ensure that `tokenIds` is strictly ascending, and perform the before hooks before any state changes.
unchecked {
// 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));
if (uint256(uint160(to)) & _BITMASK_ADDRESS == 0) revert TransferToZeroAddress();

// Disable `approvalCheck` if sender is either the owner or an approved operator for all tokens
approvalCheck = from != _msgSenderERC721A() && !isApprovedForAll(from, _msgSenderERC721A());

uint256 tokenId = _mload(i); // For checking if the `tokenIds` are strictly ascending.
// Revert if the minimum of the `tokenIds` is out of bounds.
// This is equivalent to `tokenId < _startTokenId() || end <= tokenId`.
if (end - _startTokenId() <= tokenId - _startTokenId()) _revert(OwnerQueryForNonexistentToken.selector);
_beforeTokenTransfers(from, to, tokenId, 1); // Perform the before hook.
uint256 n = tokenIds.length;

if (n >= 2) {
uint256 j = i + 0x20;
do {
uint256 next = _mload(j);
if (next <= tokenId) _revert(TokenIdsNotStrictlyAscending.selector);
_beforeTokenTransfers(from, to, tokenId = next, 1); // Perform the before hook.
} while ((j += 0x20) != e);
// Revert if the maximum of the `tokenIds` is out of bounds.
if (end <= tokenId) _revert(OwnerQueryForNonexistentToken.selector);
}
// Increment and decrement the balances.
_packedAddressData[from] -= n;
_packedAddressData[to] += n;

// The next `tokenId` to be minted (i.e. `_nextTokenId()`).
uint256 stop = _currentIndex;

// For checking if the `tokenIds` are strictly ascending.
uint256 prevTokenId;

uint256 tokenId;
uint256 currTokenId;
uint256 prevOwnershipPacked;
uint256 lastOwnershipPacked;
for (uint256 i; i != n; ) {
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();

}
uint256 prevOwnershipPacked;
do {
uint256 tokenId = _mload(i);
unchecked {
// 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;

for (uint256 j = tokenId; (prevOwnershipPacked = _packedOwnerships[j]) == uint256(0); ) --j;
// If the initialized slot is burned, revert.
if (prevOwnershipPacked & _BITMASK_BURNED != 0) revert OwnerQueryForNonexistentToken();

// Check ownership of `tokenId`.
if (address(uint160(prevOwnershipPacked)) != from) revert TransferFromIncorrectOwner();

currTokenId = tokenId;
uint256 offset;
do {
address approvedAddress = _tokenApprovals[currTokenId].value;

// Revert if the sender is not authorized to transfer the token.
if (approvalCheck) {
if (_msgSenderERC721A() != approvedAddress) revert TransferCallerNotOwnerNorApproved();
}

// Call the hook.
_beforeTokenTransfers(from, to, currTokenId, 1);

if (approvedAddress != address(0)) delete _tokenApprovals[currTokenId];

// Emit the `Transfer` event.
emit Transfer(from, to, currTokenId);
// Call the hook.
_afterTokenTransfers(from, to, 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.
(lastOwnershipPacked = _packedOwnerships[currTokenId]) == 0
);

if (prevOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector);
// Check that `tokenId` is owned by `from`.
if (uint160(prevOwnershipPacked) != fromMasked) _revert(TransferFromIncorrectOwner.selector);
// Updates tokenId:
// - `address` to the next owner.
// - `startTimestamp` to the timestamp of transfering.
// - `startTimestamp` to the timestamp of transferring.
// - `burned` to `false`.
// - `nextInitialized` to `false`.
_packedOwnerships[tokenId] = _packOwnershipData(to, _nextExtraData(from, to, prevOwnershipPacked));

// If the slot after the mini batch is neither out of bounds, nor initialized.
// If `lastOwnershipPacked == 0` we didn't break the loop due to an initialized slot.
if (currTokenId != stop)
if (lastOwnershipPacked == 0)
if (_packedOwnerships[currTokenId] == 0) _packedOwnerships[currTokenId] = prevOwnershipPacked;

// Advance `i` by `offset`, the number of tokens transferred in the mini batch.
i += offset;

// Set the `prevTokenId` for checking that the `tokenIds` is strictly ascending.
prevTokenId = currTokenId - 1;
do {
(uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId);
// Revert if the sender is not authorized to transfer the token.
if (approvalCheck)
if (by != approvedAddress) _revert(TransferCallerNotOwnerNorApproved.selector);
assembly {
if approvedAddress {
sstore(approvedAddressSlot, 0) // Equivalent to `delete _tokenApprovals[tokenId]`.
}
// Emit the `Transfer` event.
log4(0, 0, _TRANSFER_EVENT_SIGNATURE, fromMasked, toMasked, tokenId)
}
_afterTokenTransfers(from, to, tokenId, 1); // Perform the after hook.
if (_mload(i += 0x20) != ++tokenId) break; // Break if `tokenId` is not sequential.
if (i == e) break; // Break if at the end of `tokenIds`.
// Break if the packed ownership slot is initialized.
} while (_packedOwnerships[tokenId] == uint256(0));
}
}
// If the slot after the mini batch is neither out of bounds, nor initialized.
if (tokenId != end)
if (_packedOwnerships[tokenId] == uint256(0)) _packedOwnerships[tokenId] = prevOwnershipPacked;
} while (i != e);
}

/**
* @dev Equivalent to `_safeBatchTransferFrom(from, to, tokenIds, false)`.
* @dev Equivalent to `_safeBatchTransferFrom(from, to, tokenIds)`.
*/
function _safeBatchTransferFrom(
address from,
address to,
uint256[] memory tokenIds
) internal virtual {
_safeBatchTransferFrom(from, to, tokenIds, false);
_safeBatchTransferFrom(address(0), from, to, tokenIds);
}

/**
* @dev Equivalent to `_safeBatchTransferFrom(from, to, tokenIds, '', approvalCheck)`.
* @dev Equivalent to `_safeBatchTransferFrom(by, from, to, tokenIds, '')`.
*/
function _safeBatchTransferFrom(
address by,
address from,
address to,
uint256[] memory tokenIds,
bool approvalCheck
uint256[] memory tokenIds
) internal virtual {
_safeBatchTransferFrom(from, to, tokenIds, '', approvalCheck);
_safeBatchTransferFrom(by, from, to, tokenIds, '');
}

/**
* @dev Equivalent to `_safeBatchTransferFrom(from, to, tokenIds, _data, false)`.
* @dev Equivalent to `_safeBatchTransferFrom(address(0), from, to, tokenIds, _data)`.
*/
function _safeBatchTransferFrom(
address from,
address to,
uint256[] memory tokenIds,
bytes memory _data
) internal virtual {
_safeBatchTransferFrom(from, to, tokenIds, _data, false);
_safeBatchTransferFrom(address(0), from, to, tokenIds, _data);
}

/**
Expand All @@ -885,31 +865,32 @@ contract ERC721A is IERC721A {
* - `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
* - If `by` 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.
*
* `by` is the address that to check token approval for.
* If token approval check is not needed, pass in `address(0)` for `by`.
*
* Emits a {Transfer} event for each transfer.
*/
function _safeBatchTransferFrom(
address by,
address from,
address to,
uint256[] memory tokenIds,
bytes memory _data,
bool approvalCheck
bytes memory _data
) internal virtual {
_batchTransferFrom(from, to, tokenIds, approvalCheck);
_batchTransferFrom(by, from, to, tokenIds);

uint256 tokenId;
uint256 n = tokenIds.length;
unchecked {
for (uint256 i; i < n; ++i) {
tokenId = tokenIds[i];
if (to.code.length != 0)
if (!_checkContractOnERC721Received(from, to, tokenId, _data)) {
if (to.code.length != 0) {
for ((uint256 i, uint256 e) = _mdata(tokenIds); i != e; i += 0x20) {
if (!_checkContractOnERC721Received(from, to, _mload(i), _data)) {
_revert(TransferToNonERC721ReceiverImplementer.selector);
}
}
}
}
}
Expand Down Expand Up @@ -1515,11 +1496,21 @@ contract ERC721A is IERC721A {
}

/**
* @dev Branchless or.
* @dev Returns a memory pointer to the start of `a`'s data.
*/
function _mdata(uint256[] memory a) private pure returns (uint256 start, uint256 end) {
assembly {
start := add(a, 0x20)
end := add(start, shl(5, mload(a)))
}
}

/**
* @dev Returns the uint256 at `p` in memory.
*/
function _or(bool a, bool b) private pure returns (bool c) {
function _mload(uint256 p) private pure returns (uint256 result) {
assembly {
c := or(a, b)
result := mload(p)
}
}
}
6 changes: 3 additions & 3 deletions contracts/extensions/ERC721ABatchTransferable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ abstract contract ERC721ABatchTransferable is ERC721A, IERC721ABatchTransferable
address to,
uint256[] memory tokenIds
) public payable virtual override {
_batchTransferFrom(from, to, tokenIds, true);
_batchTransferFrom(_msgSenderERC721A(), from, to, tokenIds);
}

function safeBatchTransferFrom(
address from,
address to,
uint256[] memory tokenIds
) public payable virtual override {
_safeBatchTransferFrom(from, to, tokenIds, true);
_safeBatchTransferFrom(_msgSenderERC721A(), from, to, tokenIds, '');
}

function safeBatchTransferFrom(
Expand All @@ -35,6 +35,6 @@ abstract contract ERC721ABatchTransferable is ERC721A, IERC721ABatchTransferable
uint256[] memory tokenIds,
bytes memory _data
) public payable virtual override {
_safeBatchTransferFrom(from, to, tokenIds, _data, true);
_safeBatchTransferFrom(_msgSenderERC721A(), from, to, tokenIds, _data);
}
}
26 changes: 26 additions & 0 deletions contracts/mocks/ERC721AGasReporterMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ contract ERC721AGasReporterMock is ERC721A {
_mint(to, 10);
}

function safeMintHundred(address to) public {
_safeMint(to, 100);
}

function mintHundred(address to) public {
_mint(to, 100);
}

function transferTenAsc(address to) public {
unchecked {
transferFrom(msg.sender, to, 0);
Expand Down Expand Up @@ -69,4 +77,22 @@ contract ERC721AGasReporterMock is ERC721A {
transferFrom(msg.sender, to, 9);
}
}

function batchTransferHundredUnoptimized(address to) public {
unchecked {
for (uint256 i; i != 100; ++i) {
transferFrom(msg.sender, to, i);
}
}
}

function batchTransferHundredOptimized(address to) public {
unchecked {
uint256[] memory tokenIds = new uint256[](100);
for (uint256 i; i != 100; ++i) {
tokenIds[i] = i;
}
_batchTransferFrom(msg.sender, msg.sender, to, tokenIds);
}
}
}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit e24ee8c

Please sign in to comment.