616 lines
21 KiB
Solidity
616 lines
21 KiB
Solidity
// SPDX-License-Identifier: CC0-1.0
|
|
|
|
// Creator: Chiru Labs
|
|
|
|
pragma solidity ^0.8.4;
|
|
|
|
import '@openzeppelin/contracts/token/ERC721/IERC721.sol';
|
|
import '@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol';
|
|
import '@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol';
|
|
import '@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol';
|
|
import '@openzeppelin/contracts/utils/Address.sol';
|
|
import '@openzeppelin/contracts/utils/Context.sol';
|
|
import '@openzeppelin/contracts/utils/Strings.sol';
|
|
import '@openzeppelin/contracts/utils/introspection/ERC165.sol';
|
|
|
|
error ApprovalCallerNotOwnerNorApproved();
|
|
error ApprovalQueryForNonexistentToken();
|
|
error ApproveToCaller();
|
|
error ApprovalToCurrentOwner();
|
|
error BalanceQueryForZeroAddress();
|
|
error MintedQueryForZeroAddress();
|
|
error BurnedQueryForZeroAddress();
|
|
error AuxQueryForZeroAddress();
|
|
error MintToZeroAddress();
|
|
error MintZeroQuantity();
|
|
error OwnerIndexOutOfBounds();
|
|
error OwnerQueryForNonexistentToken();
|
|
error TokenIndexOutOfBounds();
|
|
error TransferCallerNotOwnerNorApproved();
|
|
error TransferFromIncorrectOwner();
|
|
error TransferToNonERC721ReceiverImplementer();
|
|
error TransferToZeroAddress();
|
|
error URIQueryForNonexistentToken();
|
|
|
|
/**
|
|
* @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including
|
|
* the Metadata extension. Built to optimize for lower gas during batch mints.
|
|
*
|
|
* Assumes serials are sequentially minted starting at _startTokenId() (defaults to 0, e.g. 0, 1, 2, 3..).
|
|
*
|
|
* Assumes that an owner cannot have more than 2**64 - 1 (max value of uint64) of supply.
|
|
*
|
|
* Assumes that the maximum token id cannot exceed 2**256 - 1 (max value of uint256).
|
|
*/
|
|
contract ERC721A is Context, ERC165, IERC721, IERC721Metadata {
|
|
using Address for address;
|
|
using Strings for uint256;
|
|
|
|
// Compiler will pack this into a single 256bit word.
|
|
struct TokenOwnership {
|
|
// The address of the owner.
|
|
address addr;
|
|
// Keeps track of the start time of ownership with minimal overhead for tokenomics.
|
|
uint64 startTimestamp;
|
|
// Whether the token has been burned.
|
|
bool burned;
|
|
}
|
|
|
|
// Compiler will pack this into a single 256bit word.
|
|
struct AddressData {
|
|
// Realistically, 2**64-1 is more than enough.
|
|
uint64 balance;
|
|
// Keeps track of mint count with minimal overhead for tokenomics.
|
|
uint64 numberMinted;
|
|
// Keeps track of burn count with minimal overhead for tokenomics.
|
|
uint64 numberBurned;
|
|
// For miscellaneous variable(s) pertaining to the address
|
|
// (e.g. number of whitelist mint slots used).
|
|
// If there are multiple variables, please pack them into a uint64.
|
|
uint64 aux;
|
|
}
|
|
|
|
// The tokenId of the next token to be minted.
|
|
uint256 internal _currentIndex;
|
|
|
|
// The number of tokens burned.
|
|
uint256 internal _burnCounter;
|
|
|
|
// Token name
|
|
string private _name;
|
|
|
|
// Token symbol
|
|
string private _symbol;
|
|
|
|
// Mapping from token ID to ownership details
|
|
// An empty struct value does not necessarily mean the token is unowned. See ownershipOf implementation for details.
|
|
mapping(uint256 => TokenOwnership) internal _ownerships;
|
|
|
|
// Mapping owner address to address data
|
|
mapping(address => AddressData) private _addressData;
|
|
|
|
// Mapping from token ID to approved address
|
|
mapping(uint256 => address) private _tokenApprovals;
|
|
|
|
// Mapping from owner to operator approvals
|
|
mapping(address => mapping(address => bool)) private _operatorApprovals;
|
|
|
|
constructor(string memory name_, string memory symbol_) {
|
|
_name = name_;
|
|
_symbol = symbol_;
|
|
_currentIndex = _startTokenId();
|
|
}
|
|
|
|
/**
|
|
* To change the starting tokenId, please override this function.
|
|
*/
|
|
function _startTokenId() internal view virtual returns (uint256) {
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC721Enumerable-totalSupply}.
|
|
* @dev Burned tokens are calculated here, use _totalMinted() if you want to count just minted tokens.
|
|
*/
|
|
function totalSupply() public view returns (uint256) {
|
|
// Counter underflow is impossible as _burnCounter cannot be incremented
|
|
// more than _currentIndex - _startTokenId() times
|
|
unchecked {
|
|
return _currentIndex - _burnCounter - _startTokenId();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the total amount of tokens minted in the contract.
|
|
*/
|
|
function _totalMinted() internal view returns (uint256) {
|
|
// Counter underflow is impossible as _currentIndex does not decrement,
|
|
// and it is initialized to _startTokenId()
|
|
unchecked {
|
|
return _currentIndex - _startTokenId();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC165-supportsInterface}.
|
|
*/
|
|
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
|
|
return
|
|
interfaceId == type(IERC721).interfaceId ||
|
|
interfaceId == type(IERC721Metadata).interfaceId ||
|
|
super.supportsInterface(interfaceId);
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC721-balanceOf}.
|
|
*/
|
|
function balanceOf(address owner) public view override returns (uint256) {
|
|
if (owner == address(0)) revert BalanceQueryForZeroAddress();
|
|
return uint256(_addressData[owner].balance);
|
|
}
|
|
|
|
/**
|
|
* Returns the number of tokens minted by `owner`.
|
|
*/
|
|
function _numberMinted(address owner) internal view returns (uint256) {
|
|
if (owner == address(0)) revert MintedQueryForZeroAddress();
|
|
return uint256(_addressData[owner].numberMinted);
|
|
}
|
|
|
|
/**
|
|
* Returns the number of tokens burned by or on behalf of `owner`.
|
|
*/
|
|
function _numberBurned(address owner) internal view returns (uint256) {
|
|
if (owner == address(0)) revert BurnedQueryForZeroAddress();
|
|
return uint256(_addressData[owner].numberBurned);
|
|
}
|
|
|
|
/**
|
|
* Returns the auxiliary data for `owner`. (e.g. number of whitelist mint slots used).
|
|
*/
|
|
function _getAux(address owner) internal view returns (uint64) {
|
|
if (owner == address(0)) revert AuxQueryForZeroAddress();
|
|
return _addressData[owner].aux;
|
|
}
|
|
|
|
/**
|
|
* Sets the auxiliary data for `owner`. (e.g. number of whitelist mint slots used).
|
|
* If there are multiple variables, please pack them into a uint64.
|
|
*/
|
|
function _setAux(address owner, uint64 aux) internal {
|
|
if (owner == address(0)) revert AuxQueryForZeroAddress();
|
|
_addressData[owner].aux = aux;
|
|
}
|
|
|
|
/**
|
|
* Gas spent here starts off proportional to the maximum mint batch size.
|
|
* It gradually moves to O(1) as tokens get transferred around in the collection over time.
|
|
*/
|
|
function ownershipOf(uint256 tokenId) internal view returns (TokenOwnership memory) {
|
|
uint256 curr = tokenId;
|
|
|
|
unchecked {
|
|
if (_startTokenId() <= curr && curr < _currentIndex) {
|
|
TokenOwnership memory ownership = _ownerships[curr];
|
|
if (!ownership.burned) {
|
|
if (ownership.addr != address(0)) {
|
|
return ownership;
|
|
}
|
|
// Invariant:
|
|
// There will always be an ownership that has an address and is not burned
|
|
// before an ownership that does not have an address and is not burned.
|
|
// Hence, curr will not underflow.
|
|
while (true) {
|
|
curr--;
|
|
ownership = _ownerships[curr];
|
|
if (ownership.addr != address(0)) {
|
|
return ownership;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
revert OwnerQueryForNonexistentToken();
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC721-ownerOf}.
|
|
*/
|
|
function ownerOf(uint256 tokenId) public view override returns (address) {
|
|
return ownershipOf(tokenId).addr;
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC721Metadata-name}.
|
|
*/
|
|
function name() public view virtual override returns (string memory) {
|
|
return _name;
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC721Metadata-symbol}.
|
|
*/
|
|
function symbol() public view virtual override returns (string memory) {
|
|
return _symbol;
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC721Metadata-tokenURI}.
|
|
*/
|
|
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
|
|
if (!_exists(tokenId)) revert URIQueryForNonexistentToken();
|
|
|
|
string memory baseURI = _baseURI();
|
|
return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : '';
|
|
}
|
|
|
|
/**
|
|
* @dev Base URI for computing {tokenURI}. If set, the resulting URI for each
|
|
* token will be the concatenation of the `baseURI` and the `tokenId`. Empty
|
|
* by default, can be overridden in child contracts.
|
|
*/
|
|
function _baseURI() internal view virtual returns (string memory) {
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC721-approve}.
|
|
*/
|
|
function approve(address to, uint256 tokenId) public override {
|
|
address owner = ERC721A.ownerOf(tokenId);
|
|
if (to == owner) revert ApprovalToCurrentOwner();
|
|
|
|
if (_msgSender() != owner && !isApprovedForAll(owner, _msgSender())) {
|
|
revert ApprovalCallerNotOwnerNorApproved();
|
|
}
|
|
|
|
_approve(to, tokenId, owner);
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC721-getApproved}.
|
|
*/
|
|
function getApproved(uint256 tokenId) public view override returns (address) {
|
|
if (!_exists(tokenId)) revert ApprovalQueryForNonexistentToken();
|
|
|
|
return _tokenApprovals[tokenId];
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC721-setApprovalForAll}.
|
|
*/
|
|
function setApprovalForAll(address operator, bool approved) public override {
|
|
if (operator == _msgSender()) revert ApproveToCaller();
|
|
|
|
_operatorApprovals[_msgSender()][operator] = approved;
|
|
emit ApprovalForAll(_msgSender(), operator, approved);
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC721-isApprovedForAll}.
|
|
*/
|
|
function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
|
|
return _operatorApprovals[owner][operator];
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC721-transferFrom}.
|
|
*/
|
|
function transferFrom(
|
|
address from,
|
|
address to,
|
|
uint256 tokenId
|
|
) public virtual override {
|
|
_transfer(from, to, tokenId);
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC721-safeTransferFrom}.
|
|
*/
|
|
function safeTransferFrom(
|
|
address from,
|
|
address to,
|
|
uint256 tokenId
|
|
) public virtual override {
|
|
safeTransferFrom(from, to, tokenId, '');
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC721-safeTransferFrom}.
|
|
*/
|
|
function safeTransferFrom(
|
|
address from,
|
|
address to,
|
|
uint256 tokenId,
|
|
bytes memory _data
|
|
) public virtual override {
|
|
_transfer(from, to, tokenId);
|
|
if (to.isContract() && !_checkContractOnERC721Received(from, to, tokenId, _data)) {
|
|
revert TransferToNonERC721ReceiverImplementer();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dev Returns whether `tokenId` exists.
|
|
*
|
|
* Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}.
|
|
*
|
|
* Tokens start existing when they are minted (`_mint`),
|
|
*/
|
|
function _exists(uint256 tokenId) internal view returns (bool) {
|
|
return _startTokenId() <= tokenId && tokenId < _currentIndex &&
|
|
!_ownerships[tokenId].burned;
|
|
}
|
|
|
|
function _safeMint(address to, uint256 quantity) internal {
|
|
_safeMint(to, quantity, '');
|
|
}
|
|
|
|
/**
|
|
* @dev Safely mints `quantity` tokens and transfers them to `to`.
|
|
*
|
|
* Requirements:
|
|
*
|
|
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called for each safe transfer.
|
|
* - `quantity` must be greater than 0.
|
|
*
|
|
* Emits a {Transfer} event.
|
|
*/
|
|
function _safeMint(
|
|
address to,
|
|
uint256 quantity,
|
|
bytes memory _data
|
|
) internal {
|
|
_mint(to, quantity, _data, true);
|
|
}
|
|
|
|
/**
|
|
* @dev Mints `quantity` tokens and transfers them to `to`.
|
|
*
|
|
* Requirements:
|
|
*
|
|
* - `to` cannot be the zero address.
|
|
* - `quantity` must be greater than 0.
|
|
*
|
|
* Emits a {Transfer} event.
|
|
*/
|
|
function _mint(
|
|
address to,
|
|
uint256 quantity,
|
|
bytes memory _data,
|
|
bool safe
|
|
) internal {
|
|
uint256 startTokenId = _currentIndex;
|
|
if (to == address(0)) revert MintToZeroAddress();
|
|
if (quantity == 0) revert MintZeroQuantity();
|
|
|
|
_beforeTokenTransfers(address(0), to, startTokenId, quantity);
|
|
|
|
// Overflows are incredibly unrealistic.
|
|
// balance or numberMinted overflow if current value of either + quantity > 1.8e19 (2**64) - 1
|
|
// updatedIndex overflows if _currentIndex + quantity > 1.2e77 (2**256) - 1
|
|
unchecked {
|
|
_addressData[to].balance += uint64(quantity);
|
|
_addressData[to].numberMinted += uint64(quantity);
|
|
|
|
_ownerships[startTokenId].addr = to;
|
|
_ownerships[startTokenId].startTimestamp = uint64(block.timestamp);
|
|
|
|
uint256 updatedIndex = startTokenId;
|
|
uint256 end = updatedIndex + quantity;
|
|
|
|
if (safe && to.isContract()) {
|
|
do {
|
|
emit Transfer(address(0), to, updatedIndex);
|
|
if (!_checkContractOnERC721Received(address(0), to, updatedIndex++, _data)) {
|
|
revert TransferToNonERC721ReceiverImplementer();
|
|
}
|
|
} while (updatedIndex != end);
|
|
// Reentrancy protection
|
|
if (_currentIndex != startTokenId) revert();
|
|
} else {
|
|
do {
|
|
emit Transfer(address(0), to, updatedIndex++);
|
|
} while (updatedIndex != end);
|
|
}
|
|
_currentIndex = updatedIndex;
|
|
}
|
|
_afterTokenTransfers(address(0), to, startTokenId, quantity);
|
|
}
|
|
|
|
/**
|
|
* @dev Transfers `tokenId` from `from` to `to`.
|
|
*
|
|
* Requirements:
|
|
*
|
|
* - `to` cannot be the zero address.
|
|
* - `tokenId` token must be owned by `from`.
|
|
*
|
|
* Emits a {Transfer} event.
|
|
*/
|
|
function _transfer(
|
|
address from,
|
|
address to,
|
|
uint256 tokenId
|
|
) private {
|
|
TokenOwnership memory prevOwnership = ownershipOf(tokenId);
|
|
|
|
bool isApprovedOrOwner = (_msgSender() == prevOwnership.addr ||
|
|
isApprovedForAll(prevOwnership.addr, _msgSender()) ||
|
|
getApproved(tokenId) == _msgSender());
|
|
|
|
if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved();
|
|
if (prevOwnership.addr != from) revert TransferFromIncorrectOwner();
|
|
if (to == address(0)) revert TransferToZeroAddress();
|
|
|
|
_beforeTokenTransfers(from, to, tokenId, 1);
|
|
|
|
// Clear approvals from the previous owner
|
|
_approve(address(0), tokenId, prevOwnership.addr);
|
|
|
|
// Underflow of the sender's balance is impossible because we check for
|
|
// ownership above and the recipient's balance can't realistically overflow.
|
|
// Counter overflow is incredibly unrealistic as tokenId would have to be 2**256.
|
|
unchecked {
|
|
_addressData[from].balance -= 1;
|
|
_addressData[to].balance += 1;
|
|
|
|
_ownerships[tokenId].addr = to;
|
|
_ownerships[tokenId].startTimestamp = uint64(block.timestamp);
|
|
|
|
// If the ownership slot of tokenId+1 is not explicitly set, that means the transfer initiator owns it.
|
|
// Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls.
|
|
uint256 nextTokenId = tokenId + 1;
|
|
if (_ownerships[nextTokenId].addr == address(0)) {
|
|
// This will suffice for checking _exists(nextTokenId),
|
|
// as a burned slot cannot contain the zero address.
|
|
if (nextTokenId < _currentIndex) {
|
|
_ownerships[nextTokenId].addr = prevOwnership.addr;
|
|
_ownerships[nextTokenId].startTimestamp = prevOwnership.startTimestamp;
|
|
}
|
|
}
|
|
}
|
|
|
|
emit Transfer(from, to, tokenId);
|
|
_afterTokenTransfers(from, to, tokenId, 1);
|
|
}
|
|
|
|
/**
|
|
* @dev Destroys `tokenId`.
|
|
* The approval is cleared when the token is burned.
|
|
*
|
|
* Requirements:
|
|
*
|
|
* - `tokenId` must exist.
|
|
*
|
|
* Emits a {Transfer} event.
|
|
*/
|
|
function _burn(uint256 tokenId) internal virtual {
|
|
TokenOwnership memory prevOwnership = ownershipOf(tokenId);
|
|
|
|
_beforeTokenTransfers(prevOwnership.addr, address(0), tokenId, 1);
|
|
|
|
// Clear approvals from the previous owner
|
|
_approve(address(0), tokenId, prevOwnership.addr);
|
|
|
|
// Underflow of the sender's balance is impossible because we check for
|
|
// ownership above and the recipient's balance can't realistically overflow.
|
|
// Counter overflow is incredibly unrealistic as tokenId would have to be 2**256.
|
|
unchecked {
|
|
_addressData[prevOwnership.addr].balance -= 1;
|
|
_addressData[prevOwnership.addr].numberBurned += 1;
|
|
|
|
// Keep track of who burned the token, and the timestamp of burning.
|
|
_ownerships[tokenId].addr = prevOwnership.addr;
|
|
_ownerships[tokenId].startTimestamp = uint64(block.timestamp);
|
|
_ownerships[tokenId].burned = true;
|
|
|
|
// If the ownership slot of tokenId+1 is not explicitly set, that means the burn initiator owns it.
|
|
// Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls.
|
|
uint256 nextTokenId = tokenId + 1;
|
|
if (_ownerships[nextTokenId].addr == address(0)) {
|
|
// This will suffice for checking _exists(nextTokenId),
|
|
// as a burned slot cannot contain the zero address.
|
|
if (nextTokenId < _currentIndex) {
|
|
_ownerships[nextTokenId].addr = prevOwnership.addr;
|
|
_ownerships[nextTokenId].startTimestamp = prevOwnership.startTimestamp;
|
|
}
|
|
}
|
|
}
|
|
|
|
emit Transfer(prevOwnership.addr, address(0), tokenId);
|
|
_afterTokenTransfers(prevOwnership.addr, address(0), tokenId, 1);
|
|
|
|
// Overflow not possible, as _burnCounter cannot be exceed _currentIndex times.
|
|
unchecked {
|
|
_burnCounter++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dev Approve `to` to operate on `tokenId`
|
|
*
|
|
* Emits a {Approval} event.
|
|
*/
|
|
function _approve(
|
|
address to,
|
|
uint256 tokenId,
|
|
address owner
|
|
) private {
|
|
_tokenApprovals[tokenId] = to;
|
|
emit Approval(owner, to, tokenId);
|
|
}
|
|
|
|
/**
|
|
* @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target contract.
|
|
*
|
|
* @param from address representing the previous owner of the given token ID
|
|
* @param to target address that will receive the tokens
|
|
* @param tokenId uint256 ID of the token to be transferred
|
|
* @param _data bytes optional data to send along with the call
|
|
* @return bool whether the call correctly returned the expected magic value
|
|
*/
|
|
function _checkContractOnERC721Received(
|
|
address from,
|
|
address to,
|
|
uint256 tokenId,
|
|
bytes memory _data
|
|
) private returns (bool) {
|
|
try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) {
|
|
return retval == IERC721Receiver(to).onERC721Received.selector;
|
|
} catch (bytes memory reason) {
|
|
if (reason.length == 0) {
|
|
revert TransferToNonERC721ReceiverImplementer();
|
|
} else {
|
|
assembly {
|
|
revert(add(32, reason), mload(reason))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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.
|
|
*
|
|
* startTokenId - the first token id to be transferred
|
|
* quantity - the amount to be transferred
|
|
*
|
|
* 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 _beforeTokenTransfers(
|
|
address from,
|
|
address to,
|
|
uint256 startTokenId,
|
|
uint256 quantity
|
|
) internal virtual {}
|
|
|
|
/**
|
|
* @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.
|
|
*
|
|
* startTokenId - the first token id to be transferred
|
|
* quantity - the amount to be transferred
|
|
*
|
|
* Calling conditions:
|
|
*
|
|
* - When `from` and `to` are both non-zero, `from`'s `tokenId` has been
|
|
* transferred to `to`.
|
|
* - When `from` is zero, `tokenId` has been minted for `to`.
|
|
* - 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 {}
|
|
} |