diff --git a/periphery/foundry.toml b/periphery/foundry.toml index 61f9154..a507ff3 100644 --- a/periphery/foundry.toml +++ b/periphery/foundry.toml @@ -43,6 +43,15 @@ override_spacing = true [doc] title = 'Aloe II' +[invariant] +runs = 128 # The number of runs for each invariant test. +depth = 100 # The number of calls executed to attempt to break invariants in one run. +fail_on_revert = true # Fails the invariant test if a revert occurs. +call_override = false # Allows overriding an unsafe external call when running invariant tests, e.g. reentrancy checks (this feature is still a WIP). +dictionary_weight = 75 # Use values collected from your contracts 75% of the time, random 25% of the time. +include_storage = true # Collect values from contract storage and add them to the dictionary. +include_push_bytes = true # Collect PUSH bytes from the contract code and add them to the dictionary. + [rpc_endpoints] mainnet = "${RPC_URL_MAINNET}" goerli = "${RPC_URL_GOERLI}" diff --git a/periphery/src/borrower-nft/BytesLib.sol b/periphery/src/borrower-nft/BytesLib.sol index dedc248..29df2e0 100644 --- a/periphery/src/borrower-nft/BytesLib.sol +++ b/periphery/src/borrower-nft/BytesLib.sol @@ -2,6 +2,8 @@ pragma solidity 0.8.17; library BytesLib { + error RemovalFailed(); + function pack(uint256[] memory items, uint256 chunkSize) internal pure returns (bytes memory newList) { uint256 shift; unchecked { @@ -111,8 +113,9 @@ library BytesLib { } } - /// @dev Removes all occurrences of `item` from `oldList`, a packed array where each element spans `chunkSize` bytes - function remove( + /// @dev Removes all occurrences of `item` from `oldList`, a packed array where each element spans + /// `chunkSize` bytes + function filter( bytes memory oldList, uint256 item, uint256 chunkSize @@ -154,6 +157,17 @@ library BytesLib { } } + /// @dev Removes all occurrences of `item` from `oldList`, a packed array where each element spans + /// `chunkSize` bytes. Reverts if nothing was removed. + function remove( + bytes memory oldList, + uint256 item, + uint256 chunkSize + ) internal pure returns (bytes memory newList) { + newList = filter(oldList, item, chunkSize); + if (newList.length == oldList.length) revert RemovalFailed(); + } + /// @dev Checks whether `item` is present in `list`, a packed array where each element spans `chunkSize` bytes function includes(bytes memory list, uint256 item, uint256 chunkSize) internal pure returns (bool result) { uint256 shift; diff --git a/periphery/src/borrower-nft/ERC721Z.sol b/periphery/src/borrower-nft/ERC721Z.sol new file mode 100644 index 0000000..e84b95d --- /dev/null +++ b/periphery/src/borrower-nft/ERC721Z.sol @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {SSTORE2} from "solady/utils/SSTORE2.sol"; + +import {BytesLib} from "./BytesLib.sol"; +import {SafeSSTORE2} from "./SafeSSTORE2.sol"; + +/** + * @title ERC721Z + * @author Aloe Labs, Inc. + * Credits: beskay0x, chiru-labs, solmate, transmissions11, nftchance, squeebo_nft and others + * @notice ERC-721 implementation optimized for minting multiple tokens at once, similar to + * [ERC721A](https://github.com/chiru-labs/ERC721A) and [ERC721B](https://github.com/beskay/ERC721B). This version allows + * tokens to have "attributes" (up to 224 bits of data stored in the `tokenId`) and enables gas-efficient queries of all + * tokens held by a given `owner`. + */ +abstract contract ERC721Z { + using SafeSSTORE2 for address; + using SafeSSTORE2 for bytes; + using BytesLib for bytes; + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + event Approval(address indexed owner, address indexed spender, uint256 indexed tokenId); + + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + /*////////////////////////////////////////////////////////////// + ATTRIBUTES STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @dev Mapping from `owner` to an SSTORE2 pointer where all their `tokenId`s are stored + /// @custom:future-work If there are properties specific to an `owner` (_not_ a token) this could map to a + /// struct instead of just an `address`. There are 96 extra bits to work with. + mapping(address => address) internal _pointers; + + /*////////////////////////////////////////////////////////////// + ERC721 STORAGE + //////////////////////////////////////////////////////////////*/ + + uint256 public totalSupply; + + /// @dev The lowest bits of `tokenId` are a counter. The counter starts at 0, and increases by 1 after each + /// mint. To get the owner of a `tokenId` with counter = i, search this mapping (beginning at the ith index and + /// moving up) until a non-zero entry is found. That entry is the owner. + mapping(uint256 => address) internal _owners; + + mapping(uint256 => address) public getApproved; + + mapping(address => mapping(address => bool)) public isApprovedForAll; + + /*////////////////////////////////////////////////////////////// + METADATA LOGIC + //////////////////////////////////////////////////////////////*/ + + function name() external view virtual returns (string memory); + + function symbol() external view virtual returns (string memory); + + function tokenURI(uint256 tokenId) external view virtual returns (string memory); + + /*////////////////////////////////////////////////////////////// + ERC721 LOGIC + //////////////////////////////////////////////////////////////*/ + + function ownerOf(uint256 tokenId) public view virtual returns (address owner) { + uint256 i = _indexOf(tokenId); + require(i < totalSupply, "NOT_MINTED"); + + unchecked { + while (true) { + owner = _owners[i]; + if (owner != address(0)) break; + i++; + } + } + + require(_pointers[owner].read().includes(tokenId, _TOKEN_SIZE()), "NOT_MINTED"); + } + + function balanceOf(address owner) public view virtual returns (uint256) { + require(owner != address(0), "ZERO_ADDRESS"); + + address pointer = _pointers[owner]; + return pointer == address(0) ? 0 : (pointer.code.length - SSTORE2.DATA_OFFSET) / _TOKEN_SIZE(); + } + + function approve(address spender, uint256 tokenId) public virtual { + address owner = ownerOf(tokenId); + + require(msg.sender == owner || isApprovedForAll[owner][msg.sender], "NOT_AUTHORIZED"); + + getApproved[tokenId] = spender; + + emit Approval(owner, spender, tokenId); + } + + function setApprovalForAll(address operator, bool approved) public virtual { + isApprovedForAll[msg.sender][operator] = approved; + + emit ApprovalForAll(msg.sender, operator, approved); + } + + function transferFrom(address from, address to, uint256 tokenId) public virtual { + require(to != address(0), "INVALID_RECIPIENT"); + require( + msg.sender == from || isApprovedForAll[from][msg.sender] || msg.sender == getApproved[tokenId], + "NOT_AUTHORIZED" + ); + + // Move `tokenId` and update storage pointers. `from` must own `tokenId` for `remove` to succeed + _pointers[from] = _pointers[from].read().remove(tokenId, _TOKEN_SIZE()).write(); + _pointers[to] = _pointers[to].read().append(tokenId, _TOKEN_SIZE()).write(); + + // Update `_owners` array + uint256 i = _indexOf(tokenId); + _owners[i] = to; + if (i > 0 && _owners[i - 1] == address(0)) { + _owners[i - 1] = from; + } + + // Delete old approval + delete getApproved[tokenId]; + + emit Transfer(from, to, tokenId); + } + + function safeTransferFrom(address from, address to, uint256 tokenId) public virtual { + safeTransferFrom(from, to, tokenId, ""); + } + + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual { + transferFrom(from, to, tokenId); + + require( + to.code.length == 0 || + ERC721TokenReceiver(to).onERC721Received(msg.sender, from, tokenId, data) == + ERC721TokenReceiver.onERC721Received.selector, + "UNSAFE_RECIPIENT" + ); + } + + /*////////////////////////////////////////////////////////////// + ERC165 LOGIC + //////////////////////////////////////////////////////////////*/ + + function supportsInterface(bytes4 interfaceId) external view virtual returns (bool) { + return + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 + interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata + } + + /*////////////////////////////////////////////////////////////// + INTERNAL MINT/BURN LOGIC + //////////////////////////////////////////////////////////////*/ + + function _mint(address to, uint256 qty, uint256[] memory attributes) internal virtual { + require(to != address(0), "INVALID_RECIPIENT"); + require(qty > 0 && qty == attributes.length, "BAD_QUANTITY"); + + unchecked { + // Increase `totalSupply` by `qty` + uint256 totalSupply_ = totalSupply; + require((totalSupply = totalSupply_ + qty) < _MAX_SUPPLY(), "MAX_SUPPLY"); + + // Set the owner of the highest minted index + _owners[totalSupply_ + qty - 1] = to; + + // Emit an event for each new token + uint256 i; + do { + attributes[i] = _tokenIdFor(totalSupply_ + i, attributes[i]); + emit Transfer(address(0), to, attributes[i]); + i++; + } while (i < qty); + } + + // Write new `tokenId`s (`attributes` array was overwritten with full `tokenId`s in the loop) + _pointers[to] = _pointers[to].read().append(attributes, _TOKEN_SIZE()).write(); + } + + /*////////////////////////////////////////////////////////////// + ATTRIBUTES LOGIC + //////////////////////////////////////////////////////////////*/ + + function _tokenIdFor(uint256 index, uint256 attributes) internal pure returns (uint256) { + return index | (attributes << (_INDEX_SIZE() << 3)); + } + + function _indexOf(uint256 tokenId) internal pure returns (uint256) { + return tokenId % _MAX_SUPPLY(); + } + + function _attributesOf(uint256 tokenId) internal pure returns (uint256) { + return tokenId >> (_INDEX_SIZE() << 3); + } + + function _MAX_SUPPLY() internal pure returns (uint256) { + return (1 << (_INDEX_SIZE() << 3)); + } + + function _TOKEN_SIZE() internal pure returns (uint256 tokenSize) { + unchecked { + tokenSize = _INDEX_SIZE() + _ATTRIBUTES_SIZE(); + // The optimizer removes this assertion; don't worry about gas + assert(tokenSize <= 32); + } + } + + /// @dev The number of bytes used to store indices. This plus `_ATTRIBUTES_SIZE` MUST be a constant <= 32. + function _INDEX_SIZE() internal pure virtual returns (uint256); + + /// @dev The number of bytes used to store attributes. This plus `_INDEX_SIZE` MUST be a constant <= 32. + function _ATTRIBUTES_SIZE() internal pure virtual returns (uint256); +} + +/// @notice A generic interface for a contract which properly accepts ERC721 tokens. +/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC721.sol) +abstract contract ERC721TokenReceiver { + function onERC721Received(address, address, uint256, bytes calldata) external virtual returns (bytes4) { + return ERC721TokenReceiver.onERC721Received.selector; + } +} diff --git a/periphery/src/borrower-nft/SafeSSTORE2.sol b/periphery/src/borrower-nft/SafeSSTORE2.sol new file mode 100644 index 0000000..6a963f9 --- /dev/null +++ b/periphery/src/borrower-nft/SafeSSTORE2.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {SSTORE2} from "solady/utils/SSTORE2.sol"; + +library SafeSSTORE2 { + /// @custom:future-work The following could be replaced with a single line, + /// `pointer = (data.length == 0) ? address(0) : SSTORE2.write(data);` + /// which is ~2% more gas efficient in most cases. However, when doing it that way, the `create` op occassionally + /// throws errors when it shouldn't (in Foundry invariant tests, as of October 2023). In an abundance of caution, + /// we've gone with the code below. It does have one positive side-effect: + /// If a piece of data has already been written somewhere, that storage contract can be reused. + function write(bytes memory data) internal returns (address pointer) { + pointer = SSTORE2.predictDeterministicAddress(data, 0, address(this)); + if (pointer.code.length == 0) { + SSTORE2.writeDeterministic(data, 0); + } + } + + function read(address pointer) internal view returns (bytes memory data) { + data = (pointer == address(0)) ? bytes("") : SSTORE2.read(pointer); + } +} diff --git a/periphery/src/nft/ERC721Z.sol b/periphery/src/nft/ERC721Z.sol deleted file mode 100644 index 2b00957..0000000 --- a/periphery/src/nft/ERC721Z.sol +++ /dev/null @@ -1,209 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.17; - -/// @notice Modern, minimalist, and gas efficient ERC-721 implementation. -/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC721.sol) -abstract contract ERC721 { - /*////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ - - event Transfer(address indexed from, address indexed to, uint256 indexed id); - - event Approval(address indexed owner, address indexed spender, uint256 indexed id); - - event ApprovalForAll(address indexed owner, address indexed operator, bool approved); - - /*////////////////////////////////////////////////////////////// - METADATA STORAGE/LOGIC - //////////////////////////////////////////////////////////////*/ - - string public name; - - string public symbol; - - function tokenURI(uint256 id) public view virtual returns (string memory); - - /*////////////////////////////////////////////////////////////// - ERC721 BALANCE/OWNER STORAGE - //////////////////////////////////////////////////////////////*/ - - mapping(uint256 => address) internal _ownerOf; - - mapping(address => uint256) internal _balanceOf; - - function ownerOf(uint256 id) public view virtual returns (address owner) { - require((owner = _ownerOf[id]) != address(0), "NOT_MINTED"); - } - - function balanceOf(address owner) public view virtual returns (uint256) { - require(owner != address(0), "ZERO_ADDRESS"); - - return _balanceOf[owner]; - } - - /*////////////////////////////////////////////////////////////// - ERC721 APPROVAL STORAGE - //////////////////////////////////////////////////////////////*/ - - mapping(uint256 => address) public getApproved; - - mapping(address => mapping(address => bool)) public isApprovedForAll; - - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - constructor(string memory _name, string memory _symbol) { - name = _name; - symbol = _symbol; - } - - /*////////////////////////////////////////////////////////////// - ERC721 LOGIC - //////////////////////////////////////////////////////////////*/ - - function approve(address spender, uint256 id) public virtual { - address owner = _ownerOf[id]; - - require(msg.sender == owner || isApprovedForAll[owner][msg.sender], "NOT_AUTHORIZED"); - - getApproved[id] = spender; - - emit Approval(owner, spender, id); - } - - function setApprovalForAll(address operator, bool approved) public virtual { - isApprovedForAll[msg.sender][operator] = approved; - - emit ApprovalForAll(msg.sender, operator, approved); - } - - function transferFrom(address from, address to, uint256 id) public virtual { - require(from == _ownerOf[id], "WRONG_FROM"); - - require(to != address(0), "INVALID_RECIPIENT"); - - require( - msg.sender == from || isApprovedForAll[from][msg.sender] || msg.sender == getApproved[id], - "NOT_AUTHORIZED" - ); - - // Underflow of the sender's balance is impossible because we check for - // ownership above and the recipient's balance can't realistically overflow. - unchecked { - _balanceOf[from]--; - - _balanceOf[to]++; - } - - _ownerOf[id] = to; - - delete getApproved[id]; - - emit Transfer(from, to, id); - } - - function safeTransferFrom(address from, address to, uint256 id) public virtual { - transferFrom(from, to, id); - - require( - to.code.length == 0 || - ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") == - ERC721TokenReceiver.onERC721Received.selector, - "UNSAFE_RECIPIENT" - ); - } - - function safeTransferFrom(address from, address to, uint256 id, bytes calldata data) public virtual { - transferFrom(from, to, id); - - require( - to.code.length == 0 || - ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) == - ERC721TokenReceiver.onERC721Received.selector, - "UNSAFE_RECIPIENT" - ); - } - - /*////////////////////////////////////////////////////////////// - ERC165 LOGIC - //////////////////////////////////////////////////////////////*/ - - function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { - return - interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 - interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 - interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata - } - - /*////////////////////////////////////////////////////////////// - INTERNAL MINT/BURN LOGIC - //////////////////////////////////////////////////////////////*/ - - function _mint(address to, uint256 id) internal virtual { - require(to != address(0), "INVALID_RECIPIENT"); - - require(_ownerOf[id] == address(0), "ALREADY_MINTED"); - - // Counter overflow is incredibly unrealistic. - unchecked { - _balanceOf[to]++; - } - - _ownerOf[id] = to; - - emit Transfer(address(0), to, id); - } - - function _burn(uint256 id) internal virtual { - address owner = _ownerOf[id]; - - require(owner != address(0), "NOT_MINTED"); - - // Ownership check above ensures no underflow. - unchecked { - _balanceOf[owner]--; - } - - delete _ownerOf[id]; - - delete getApproved[id]; - - emit Transfer(owner, address(0), id); - } - - /*////////////////////////////////////////////////////////////// - INTERNAL SAFE MINT LOGIC - //////////////////////////////////////////////////////////////*/ - - function _safeMint(address to, uint256 id) internal virtual { - _mint(to, id); - - require( - to.code.length == 0 || - ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, "") == - ERC721TokenReceiver.onERC721Received.selector, - "UNSAFE_RECIPIENT" - ); - } - - function _safeMint(address to, uint256 id, bytes memory data) internal virtual { - _mint(to, id); - - require( - to.code.length == 0 || - ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, data) == - ERC721TokenReceiver.onERC721Received.selector, - "UNSAFE_RECIPIENT" - ); - } -} - -/// @notice A generic interface for a contract which properly accepts ERC721 tokens. -/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC721.sol) -abstract contract ERC721TokenReceiver { - function onERC721Received(address, address, uint256, bytes calldata) external virtual returns (bytes4) { - return ERC721TokenReceiver.onERC721Received.selector; - } -} diff --git a/periphery/test/borrower-nft/BytesLib.t.sol b/periphery/test/borrower-nft/BytesLib.t.sol index 26ef1a8..4ab592a 100644 --- a/periphery/test/borrower-nft/BytesLib.t.sol +++ b/periphery/test/borrower-nft/BytesLib.t.sol @@ -185,10 +185,10 @@ contract BytesLibTest is Test { } /*////////////////////////////////////////////////////////////// - REMOVE + FILTER //////////////////////////////////////////////////////////////*/ - function test_remove(bytes calldata raw, uint256 item, uint256 chunkSize) public { + function test_filter(bytes calldata raw, uint256 item, uint256 chunkSize) public { chunkSize = bound(chunkSize, 1, 28); item = bound(item, 0, (1 << (chunkSize << 3)) - 1); @@ -198,34 +198,34 @@ contract BytesLibTest is Test { data = raw[0:length]; } - bytes memory newList = data.remove(item, chunkSize); + bytes memory newList = data.filter(item, chunkSize); assertFalse(newList.includes(item, chunkSize)); data = data.append(item, chunkSize); data = bytes.concat(data, data); assertTrue(data.includes(item, chunkSize)); - newList = data.remove(item, chunkSize); + newList = data.filter(item, chunkSize); assertFalse(newList.includes(item, chunkSize)); } - function test_spec_remove(uint256 x) public { + function test_spec_filter(uint256 x) public { vm.assume(x != 12 && x != 34 && x != 56 && x != 78); bytes memory a = abi.encodePacked(uint56(12), uint56(34), uint56(56), uint56(78)); - bytes memory b = a.remove(12, 7); + bytes memory b = a.filter(12, 7); assertEq(b, abi.encodePacked(uint56(34), uint56(56), uint56(78))); - b = a.remove(34, 7); + b = a.filter(34, 7); assertEq(b, abi.encodePacked(uint56(12), uint56(56), uint56(78))); - b = a.remove(56, 7); + b = a.filter(56, 7); assertEq(b, abi.encodePacked(uint56(12), uint56(34), uint56(78))); - b = a.remove(78, 7); + b = a.filter(78, 7); assertEq(b, abi.encodePacked(uint56(12), uint56(34), uint56(56))); - b = a.remove(x, 7); + b = a.filter(x, 7); assertEq(b, a); } diff --git a/periphery/test/borrower-nft/ERC721Z.t.sol b/periphery/test/borrower-nft/ERC721Z.t.sol new file mode 100644 index 0000000..2cbd286 --- /dev/null +++ b/periphery/test/borrower-nft/ERC721Z.t.sol @@ -0,0 +1,373 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "forge-std/Test.sol"; + +import {ERC721} from "solmate/tokens/ERC721.sol"; + +import {ERC721Z, SafeSSTORE2, BytesLib} from "src/borrower-nft/ERC721Z.sol"; + +contract MockERC721Z is ERC721Z { + function name() external pure override returns (string memory) { + return "Mock NFT"; + } + + function symbol() external pure override returns (string memory) { + return "MOCK"; + } + + function tokenURI(uint256) external pure override returns (string memory) { + return ""; + } + + /// @inheritdoc ERC721Z + function _INDEX_SIZE() internal pure override returns (uint256) { + return 2; + } + + /// @inheritdoc ERC721Z + function _ATTRIBUTES_SIZE() internal pure override returns (uint256) { + return 20; + } + + function hasToken(address owner, uint256 tokenId) external view returns (bool) { + return BytesLib.includes(SafeSSTORE2.read(_pointers[owner]), tokenId, _TOKEN_SIZE()); + } + + function mint(address to, uint256 qty, uint256[] calldata attributes) external { + _mint(to, qty, attributes); + } +} + +contract ERC721Baseline is ERC721 { + uint256 public totalSupply; + + mapping(address => mapping(uint256 => bool)) public hasToken; + + constructor() ERC721("Baseline", "BASE") {} + + function tokenURI(uint256) public view virtual override returns (string memory) { + return ""; + } + + function transferFrom(address from, address to, uint256 id) public override { + hasToken[from][id] = false; + hasToken[to][id] = true; + + super.transferFrom(from, to, id); + } + + function mint(address to, uint256 qty, uint256[] calldata attributes) external { + require(qty > 0 && qty == attributes.length, "BAD_QUANTITY"); + + for (uint256 i; i < qty; i++) { + uint256 tokenId = (attributes[i] << 32) + ((totalSupply + i) % (1 << 32)); + + hasToken[to][tokenId] = true; + super._mint(to, tokenId); + } + totalSupply += qty; + } +} + +contract Harness { + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + Vm constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + MockERC721Z immutable MOCK; + + ERC721Baseline immutable BASELINE; + + mapping(address => bool) internal _isOwner; + + address[] public owners; + + mapping(uint256 => bool) internal _isTokenId; + + uint256[] public tokenIds; + + constructor(MockERC721Z mock, ERC721Baseline baseline) { + MOCK = mock; + BASELINE = baseline; + } + + function approve(address caller, address spender, uint256 id) public { + if (!_isTokenId[id]) { + vm.expectRevert(bytes("NOT_MINTED")); + MOCK.approve(spender, id); + + if (tokenIds.length == 0) return; + id = tokenIds[id % tokenIds.length]; + } + + vm.startPrank(caller); + address owner = BASELINE.ownerOf(id); + if (!(caller == owner || BASELINE.isApprovedForAll(owner, caller))) { + vm.expectRevert(bytes("NOT_AUTHORIZED")); + MOCK.approve(spender, id); + vm.expectRevert(bytes("NOT_AUTHORIZED")); + BASELINE.approve(spender, id); + + vm.stopPrank(); + caller = owner; + vm.startPrank(caller); + } + + MOCK.approve(spender, id); + BASELINE.approve(spender, id); + vm.stopPrank(); + } + + function setApprovalForAll(address caller, address operator, bool approved) public { + vm.prank(caller); + MOCK.setApprovalForAll(operator, approved); + vm.prank(caller); + BASELINE.setApprovalForAll(operator, approved); + } + + function mint(address to, uint256 qty) external { + if (to == address(0)) to = address(1); + qty = (qty % 16) + 1; + + uint256[] memory attributes = new uint256[](qty); + uint256 balance = BASELINE.balanceOf(to); + uint256 totalSupply = BASELINE.totalSupply(); + for (uint256 i; i < qty; i++) { + // `uint160` assumes that `_ATTRIBUTE_SIZE` is 20! + attributes[i] = uint160(uint256(keccak256(abi.encodePacked(to, totalSupply + balance + i)))); + } + + if (to == address(0)) { + vm.expectRevert(bytes("INVALID_RECIPIENT")); + MOCK.mint(to, qty, attributes); + vm.expectRevert(bytes("INVALID_RECIPIENT")); + BASELINE.mint(to, qty, attributes); + to = address(12345); + } + + // SSTORE2 can only handle up to 24,576 bytes + if (balance + qty >= uint256(24476) / 20) { + vm.expectRevert(0x30116425); + MOCK.mint(to, qty, attributes); + return; + } + + // Expected events + for (uint256 i; i < qty; i++) { + uint256 tokenId = (attributes[i] << 32) + (totalSupply + i); + _isTokenId[tokenId] = true; + tokenIds.push(tokenId); + + vm.expectEmit(true, true, true, false, address(MOCK)); + emit Transfer(address(0), to, tokenId); + } + + // ERC721Z + MOCK.mint(to, qty, attributes); + + // Ghost + BASELINE.mint(to, qty, attributes); + + // {harness bookkeeping} + if (!_isOwner[to]) { + _isOwner[to] = true; + owners.push(to); + } + } + + function transferFrom(address caller, address from, address to, uint256 tokenId) external { + if (to == address(0)) { + vm.expectRevert(bytes("INVALID_RECIPIENT")); + MOCK.transferFrom(from, to, tokenId); + + if (owners.length == 0) return; + to = owners[tokenId % owners.length]; + } + + if (!_isTokenId[tokenId]) { + vm.prank(from); + vm.expectRevert(BytesLib.RemovalFailed.selector); + MOCK.transferFrom(from, to, tokenId); + + if (tokenIds.length == 0) return; + tokenId = tokenIds[tokenId % tokenIds.length]; + } + + if (BASELINE.ownerOf(tokenId) != from) { + vm.prank(from); + vm.expectRevert(BytesLib.RemovalFailed.selector); + MOCK.transferFrom(from, to, tokenId); + vm.expectRevert(bytes("WRONG_FROM")); + BASELINE.transferFrom(from, to, tokenId); + + from = BASELINE.ownerOf(tokenId); + } + + if (!(caller == from || BASELINE.isApprovedForAll(from, caller) || caller == BASELINE.getApproved(tokenId))) { + vm.prank(caller); + vm.expectRevert(bytes("NOT_AUTHORIZED")); + MOCK.transferFrom(from, to, tokenId); + vm.prank(caller); + vm.expectRevert(bytes("NOT_AUTHORIZED")); + BASELINE.transferFrom(from, to, tokenId); + + caller = from; + } + + // SSTORE2 can only handle up to 24,576 bytes + if (BASELINE.balanceOf(to) + 1 >= uint256(24476) / 20) { + vm.expectRevert(0x30116425); + vm.prank(caller); + MOCK.transferFrom(from, to, tokenId); + return; + } + + vm.prank(caller); + vm.expectEmit(true, true, true, false, address(MOCK)); + emit Transfer(from, to, tokenId); + MOCK.transferFrom(from, to, tokenId); + + vm.prank(caller); + BASELINE.transferFrom(from, to, tokenId); + + // {harness bookkeeping} + if (!_isOwner[to]) { + _isOwner[to] = true; + owners.push(to); + } + } + + function getNumOwners() external view returns (uint256) { + return owners.length; + } + + function getNumTokenIds() external view returns (uint256) { + return tokenIds.length; + } +} + +contract ERC721ZTest is Test { + MockERC721Z mock; + + ERC721Baseline baseline; + + Harness harness; + + function setUp() public { + mock = new MockERC721Z(); + baseline = new ERC721Baseline(); + harness = new Harness(mock, baseline); + + targetContract(address(harness)); + + excludeSender(address(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D)); // vm + excludeSender(address(0x4e59b44847b379578588920cA78FbF26c0B4956C)); // built-in create2 deployer + excludeSender(address(this)); + excludeSender(address(mock)); + excludeSender(address(baseline)); + excludeSender(address(harness)); + } + + function invariant_totalSupply() public { + assertEq(mock.totalSupply(), baseline.totalSupply()); + } + + function invariant_ownerOf() public { + uint256 numTokens = harness.getNumTokenIds(); + for (uint256 i; i < numTokens; i++) { + uint256 tokenId = harness.tokenIds(i); + assertEq(mock.ownerOf(tokenId), baseline.ownerOf(tokenId)); + } + } + + function invariant_balanceOf() public { + uint256 numOwners = harness.getNumOwners(); + for (uint256 i; i < numOwners; i++) { + address owner = harness.owners(i); + assertEq(mock.balanceOf(owner), baseline.balanceOf(owner)); + } + } + + function invariant_hasAttributes() public { + uint256 numOwners = harness.getNumOwners(); + uint256 numTokens = harness.getNumTokenIds(); + for (uint256 i; i < numOwners; i++) { + address owner = harness.owners(i); + + for (uint256 j; j < numTokens; j++) { + uint256 tokenId = harness.tokenIds(j) % (1 << 224); + + assertEq(mock.hasToken(owner, tokenId), baseline.hasToken(owner, tokenId)); + } + } + } + + function test_gas_mint1(uint128 a) public { + uint256[] memory arr = new uint256[](1); + arr[0] = a; + mock.mint(address(1), 1, arr); + } + + function test_gas_mint2(uint128 a, uint128 b) public { + uint256[] memory arr = new uint256[](2); + arr[0] = a; + arr[1] = b; + mock.mint(address(1), 2, arr); + } + + function test_gas_mint4(uint128 a, uint128 b, uint128 c, uint128 d) public { + uint256[] memory arr = new uint256[](4); + arr[0] = a; + arr[1] = b; + arr[2] = c; + arr[3] = d; + mock.mint(address(1), 4, arr); + } + + function test_gas_reuseImpossible(uint128 a, uint128 b, uint128 c, uint128 d, uint128 e) public { + vm.pauseGasMetering(); + uint256[] memory arr = new uint256[](4); + arr[0] = a; + arr[1] = b; + arr[2] = c; + arr[3] = d; + mock.mint(address(1), 4, arr); + + arr = new uint256[](1); + arr[0] = e; + mock.mint(address(2), 1, arr); + + uint256 id = (uint256(a) << 16) + uint256(0); + vm.prank(address(1)); + mock.transferFrom(address(1), address(2), id); + + id = (uint256(e) << 16) + uint256(4); + vm.resumeGasMetering(); + vm.prank(address(2)); + mock.transferFrom(address(2), address(1), id); + } + + function test_gas_reusePossible(uint128 a, uint128 b, uint128 c, uint128 d, uint128 e) public { + vm.pauseGasMetering(); + uint256[] memory arr = new uint256[](4); + arr[0] = a; + arr[1] = b; + arr[2] = c; + arr[3] = d; + mock.mint(address(1), 4, arr); + + arr = new uint256[](1); + arr[0] = e; + mock.mint(address(2), 1, arr); + + uint256 id = (uint256(a) << 16) + uint256(0); + vm.prank(address(1)); + mock.transferFrom(address(1), address(2), id); + + id = (uint256(a) << 16) + uint256(0); + vm.resumeGasMetering(); + vm.prank(address(2)); + mock.transferFrom(address(2), address(1), id); + } +}