-
Notifications
You must be signed in to change notification settings - Fork 842
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added burn functionality #61
Changes from 25 commits
d92c3d0
4a8bc00
00fe576
2052461
33d4b41
5a6c851
a2ef813
07573e9
bfd6d97
8fc600f
e23e803
8ff5722
a66b24b
e1df945
9820bcb
7a02958
1cf3bc1
f25c639
92c82e1
66b488f
39ff829
212912c
bcbfb94
1f3bc9e
a06b5bc
64bc7e7
01e7ade
2e4857d
5ac0204
53ca717
d08c926
c493bf1
492ec57
7cf1fca
1aeb1b8
1bc34da
6e1ab7a
9ee026c
314faba
c539195
ca9bb69
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ error ApproveToCaller(); | |
error ApprovalToCurrentOwner(); | ||
error BalanceQueryForZeroAddress(); | ||
error MintedQueryForZeroAddress(); | ||
error BurnedQueryForZeroAddress(); | ||
error MintToZeroAddress(); | ||
error MintZeroQuantity(); | ||
error OwnerIndexOutOfBounds(); | ||
|
@@ -27,35 +28,47 @@ error TransferCallerNotOwnerNorApproved(); | |
error TransferFromIncorrectOwner(); | ||
error TransferToNonERC721ReceiverImplementer(); | ||
error TransferToZeroAddress(); | ||
error UnableDetermineTokenOwner(); | ||
error UnableGetTokenOwnerByIndex(); | ||
error URIQueryForNonexistentToken(); | ||
error SafecastOverflow(); | ||
|
||
/** | ||
* @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including | ||
* the Metadata and Enumerable extension. Built to optimize for lower gas during batch mints. | ||
* | ||
* Assumes serials are sequentially minted starting at 0 (e.g. 0, 1, 2, 3..). | ||
* | ||
* Does not support burning tokens to address(0). | ||
* Assumes that an owner cannot have more than 2**64 - 1 (max value of uint64) of supply. | ||
* | ||
* Assumes that an owner cannot have more than the 2**128 - 1 (max value of uint128) of supply | ||
* Assumes that the maximum token id cannot exceed 2**128 - 1 (max value of uint128). | ||
*/ | ||
contract ERC721A is Context, ERC165, IERC721, IERC721Metadata, IERC721Enumerable { | ||
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 { | ||
uint128 balance; | ||
uint128 numberMinted; | ||
// 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; | ||
} | ||
|
||
uint256 internal currentIndex; | ||
uint128 internal currentIndex; | ||
|
||
uint128 internal burnCounter; | ||
|
||
// Token name | ||
string private _name; | ||
|
@@ -85,15 +98,36 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata, IERC721Enumerable | |
* @dev See {IERC721Enumerable-totalSupply}. | ||
*/ | ||
function totalSupply() public view override returns (uint256) { | ||
return currentIndex; | ||
// Counter underflow is impossible as burnCounter cannot be incremented | ||
// more than currentIndex times | ||
unchecked { | ||
return currentIndex - burnCounter; | ||
} | ||
} | ||
|
||
/** | ||
* @dev See {IERC721Enumerable-tokenByIndex}. | ||
* This read function is O(totalSupply). If calling from a separate contract, be sure to test gas first. | ||
* It may also degrade with extremely large collection sizes (e.g >> 10000), test for your use case. | ||
*/ | ||
function tokenByIndex(uint256 index) public view override returns (uint256) { | ||
if (index >= totalSupply()) revert TokenIndexOutOfBounds(); | ||
return index; | ||
uint256 numMintedSoFar = currentIndex; | ||
uint256 tokenIdsIdx; | ||
|
||
// Counter overflow is impossible as the loop breaks when | ||
// uint256 i is equal to another uint256 numMintedSoFar. | ||
unchecked { | ||
for (uint256 i; i < numMintedSoFar; i++) { | ||
TokenOwnership memory ownership = _ownerships[i]; | ||
if (!ownership.burned) { | ||
if (tokenIdsIdx == index) { | ||
return i; | ||
} | ||
tokenIdsIdx++; | ||
} | ||
} | ||
} | ||
revert TokenIndexOutOfBounds(); | ||
} | ||
|
||
/** | ||
|
@@ -103,14 +137,18 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata, IERC721Enumerable | |
*/ | ||
function tokenOfOwnerByIndex(address owner, uint256 index) public view override returns (uint256) { | ||
if (index >= balanceOf(owner)) revert OwnerIndexOutOfBounds(); | ||
uint256 numMintedSoFar = totalSupply(); | ||
uint256 numMintedSoFar = currentIndex; | ||
uint256 tokenIdsIdx; | ||
address currOwnershipAddr; | ||
|
||
// Counter overflow is impossible as the loop breaks when uint256 i is equal to another uint256 numMintedSoFar. | ||
// Counter overflow is impossible as the loop breaks when | ||
// uint256 i is equal to another uint256 numMintedSoFar. | ||
unchecked { | ||
for (uint256 i; i < numMintedSoFar; i++) { | ||
TokenOwnership memory ownership = _ownerships[i]; | ||
if (ownership.burned) { | ||
continue; | ||
} | ||
if (ownership.addr != address(0)) { | ||
currOwnershipAddr = ownership.addr; | ||
} | ||
|
@@ -122,7 +160,6 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata, IERC721Enumerable | |
} | ||
} | ||
} | ||
|
||
revert UnableGetTokenOwnerByIndex(); | ||
} | ||
|
||
|
@@ -150,23 +187,37 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata, IERC721Enumerable | |
return uint256(_addressData[owner].numberMinted); | ||
} | ||
|
||
function _numberBurned(address owner) internal view returns (uint256) { | ||
if (owner == address(0)) revert BurnedQueryForZeroAddress(); | ||
return uint256(_addressData[owner].numberBurned); | ||
} | ||
|
||
/** | ||
* 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) { | ||
if (!_exists(tokenId)) revert OwnerQueryForNonexistentToken(); | ||
uint256 curr = tokenId; | ||
|
||
// Underflow is impossible because curr must be > 0 before decrement. | ||
unchecked { | ||
for (uint256 curr = tokenId; curr >= 0; curr--) { | ||
if (curr < currentIndex) { | ||
TokenOwnership memory ownership = _ownerships[curr]; | ||
if (ownership.addr != address(0)) { | ||
return ownership; | ||
if (!ownership.burned) { | ||
if (ownership.addr != address(0)) { | ||
return ownership; | ||
} | ||
Comment on lines
+210
to
+213
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we generally don't expect too many burns, it might be more optimal to check burned after L207 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Burn check has to be done for all cases.
|
||
while (curr > 0) { | ||
curr--; | ||
ownership = _ownerships[curr]; | ||
if (ownership.addr != address(0)) { | ||
return ownership; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
revert UnableDetermineTokenOwner(); | ||
revert OwnerQueryForNonexistentToken(); | ||
} | ||
|
||
/** | ||
|
@@ -216,8 +267,10 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata, IERC721Enumerable | |
address owner = ERC721A.ownerOf(tokenId); | ||
if (to == owner) revert ApprovalToCurrentOwner(); | ||
|
||
if (_msgSender() != owner && !isApprovedForAll(owner, _msgSender())) revert ApprovalCallerNotOwnerNorApproved(); | ||
|
||
if (_msgSender() != owner && !isApprovedForAll(owner, _msgSender())) { | ||
revert ApprovalCallerNotOwnerNorApproved(); | ||
} | ||
|
||
_approve(to, tokenId, owner); | ||
} | ||
|
||
|
@@ -277,9 +330,11 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata, IERC721Enumerable | |
address to, | ||
uint256 tokenId, | ||
bytes memory _data | ||
) public override { | ||
) public virtual override { | ||
_transfer(from, to, tokenId); | ||
if (!_checkOnERC721Received(from, to, tokenId, _data)) revert TransferToNonERC721ReceiverImplementer(); | ||
if (!_checkOnERC721Received(from, to, tokenId, _data)) { | ||
revert TransferToNonERC721ReceiverImplementer(); | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -290,7 +345,7 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata, IERC721Enumerable | |
* Tokens start existing when they are minted (`_mint`), | ||
*/ | ||
function _exists(uint256 tokenId) internal view returns (bool) { | ||
return tokenId < currentIndex; | ||
return tokenId < currentIndex && !_ownerships[tokenId].burned; | ||
Vectorized marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
function _safeMint(address to, uint256 quantity) internal { | ||
|
@@ -339,10 +394,10 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata, IERC721Enumerable | |
|
||
// Overflows are incredibly unrealistic. | ||
// balance or numberMinted overflow if current value of either + quantity > 3.4e38 (2**128) - 1 | ||
// updatedIndex overflows if currentIndex + quantity > 1.56e77 (2**256) - 1 | ||
// updatedIndex overflows if currentIndex + quantity > 3.4e38 (2**128) - 1 | ||
unchecked { | ||
_addressData[to].balance += uint128(quantity); | ||
_addressData[to].numberMinted += uint128(quantity); | ||
_addressData[to].balance += uint64(quantity); | ||
_addressData[to].numberMinted += uint64(quantity); | ||
|
||
_ownerships[startTokenId].addr = to; | ||
_ownerships[startTokenId].startTimestamp = uint64(block.timestamp); | ||
|
@@ -354,13 +409,12 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata, IERC721Enumerable | |
if (safe && !_checkOnERC721Received(address(0), to, updatedIndex, _data)) { | ||
revert TransferToNonERC721ReceiverImplementer(); | ||
} | ||
|
||
updatedIndex++; | ||
} | ||
|
||
currentIndex = updatedIndex; | ||
if (updatedIndex > type(uint128).max) revert SafecastOverflow(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we make assumptions a lot that we won't encounter overflow, is there a reason we're explicitly checking for it here? If we do want to check i would prefer a separate PR that does similar checks everywhere and allows for discussion There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the same, also I think all the changes in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can remove the overflow check. Most if not all users won't need the check. Even if the startIndex can be customized (in the future), most users will choose either 0 or 1, so overflow will still be very unrealistic. I think leaving a cautionary comment will suffice in that case. As for packing the |
||
currentIndex = uint128(updatedIndex); | ||
} | ||
|
||
_afterTokenTransfers(address(0), to, startTokenId, quantity); | ||
} | ||
|
||
|
@@ -396,10 +450,10 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata, IERC721Enumerable | |
|
||
// 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. | ||
// Counter overflow is incredibly unrealistic as tokenId would have to be 2**128. | ||
unchecked { | ||
_addressData[from].balance -= 1; | ||
_addressData[to].balance += 1; | ||
_addressData[to].balance += 1; | ||
|
||
_ownerships[tokenId].addr = to; | ||
_ownerships[tokenId].startTimestamp = uint64(block.timestamp); | ||
|
@@ -408,7 +462,9 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata, IERC721Enumerable | |
// 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)) { | ||
if (_exists(nextTokenId)) { | ||
// 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; | ||
} | ||
|
@@ -419,6 +475,55 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata, IERC721Enumerable | |
_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**128. | ||
unchecked { | ||
_addressData[prevOwnership.addr].balance -= 1; | ||
_addressData[prevOwnership.addr].numberBurned += 1; | ||
|
||
// Keep track of who burnt the token, and when is it burned. | ||
_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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could be worth making this a helper function and re-use it here and in the _transfer function, but I don't have strong opinions here since you only have it in two places |
||
// 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); | ||
|
||
burnCounter++; | ||
} | ||
|
||
/** | ||
* @dev Approve `to` to operate on `tokenId` | ||
* | ||
|
@@ -453,8 +558,9 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata, IERC721Enumerable | |
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 { | ||
if (reason.length == 0) { | ||
revert TransferToNonERC721ReceiverImplementer(); | ||
} else { | ||
assembly { | ||
revert(add(32, reason), mload(reason)) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// SPDX-License-Identifier: MIT | ||
// Creator: Chiru Labs | ||
|
||
pragma solidity ^0.8.4; | ||
|
||
import '../ERC721A.sol'; | ||
import '@openzeppelin/contracts/utils/Context.sol'; | ||
|
||
/** | ||
* @title ERC721A Burnable Token | ||
* @dev ERC721A Token that can be irreversibly burned (destroyed). | ||
*/ | ||
abstract contract ERC721ABurnable is Context, ERC721A { | ||
|
||
/** | ||
* @dev Burns `tokenId`. See {ERC721A-_burn}. | ||
* | ||
* Requirements: | ||
* | ||
* - The caller must own `tokenId` or be an approved operator. | ||
*/ | ||
function burn(uint256 tokenId) public virtual { | ||
TokenOwnership memory prevOwnership = ownershipOf(tokenId); | ||
|
||
bool isApprovedOrOwner = (_msgSender() == prevOwnership.addr || | ||
getApproved(tokenId) == _msgSender() || | ||
isApprovedForAll(prevOwnership.addr, _msgSender())); | ||
|
||
if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved(); | ||
|
||
_burn(tokenId); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason why you decided to include this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keeping track of numberMinted and the block timestamps for ownerships are not in the spec, but are useful to squeeze the most out of each SSTORE. Going by the same spirit, numberBurned is included.
If you prefer, it can be removed or moved to a future PR. Let me know what you think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can keep this in for now - was just curious. We're still going through this PR, just been a super busy week. Will try to get this done soon!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of the currently opened feature PRs, which ones likely to be merged before this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Merged the custom errors from #73.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the next PR I'm reviewing