diff --git a/lib/forge-std b/lib/forge-std index 24d76395..b8f065fd 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 24d76395806bf34aaab7ce0291094174631487f6 +Subproject commit b8f065fda83b8cd94a6b2fec8fcd911dc3b444fd diff --git a/test/token/ERC721/ERC721Enumerable/ERC721EnumerableFacet.t.sol b/test/token/ERC721/ERC721Enumerable/ERC721EnumerableFacet.t.sol new file mode 100644 index 00000000..f78d2e47 --- /dev/null +++ b/test/token/ERC721/ERC721Enumerable/ERC721EnumerableFacet.t.sol @@ -0,0 +1,439 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {ERC721EnumerableFacetHarness} from "./harnesses/ERC721EnumerableFacetHarness.sol"; +import {ERC721EnumerableFacet} from "../../../../src/token/ERC721/ERC721Enumerable/ERC721EnumerableFacet.sol"; + +contract ERC721EnumerableFacetTest is Test { + ERC721EnumerableFacetHarness public harness; + + address public alice; + address public bob; + address public charlie; + + string constant TOKEN_NAME = "Test Token"; + string constant TOKEN_SYMBOL = "TEST"; + string constant BASE_URI = "https://example.com/api/nft/"; + + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + event Approval(address indexed _owner, address indexed _to, uint256 indexed _tokenId); + event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); + + function setUp() public { + alice = makeAddr("alice"); + bob = makeAddr("bob"); + charlie = makeAddr("charlie"); + + harness = new ERC721EnumerableFacetHarness(); + harness.initialize(TOKEN_NAME, TOKEN_SYMBOL, BASE_URI); + } + + // ============================================ + // Metadata Tests + // ============================================ + + function test_Name() public view { + assertEq(harness.name(), TOKEN_NAME); + } + + function test_Symbol() public view { + assertEq(harness.symbol(), TOKEN_SYMBOL); + } + + function test_TokenURI() public { + uint256 tokenId = 1; + string memory expectedURI = string(abi.encodePacked(BASE_URI, "1")); + + harness.mint(alice, tokenId); + + string memory tokenURI = harness.tokenURI(tokenId); + assertEq(tokenURI, expectedURI); + } + + function test_TokenURIWithZeroTokenId() public { + uint256 tokenId = 0; + string memory expectedURI = string(abi.encodePacked(BASE_URI, "0")); + + harness.mint(alice, tokenId); + + string memory tokenURI = harness.tokenURI(tokenId); + assertEq(tokenURI, expectedURI); + } + + function test_TokenURIRevertWhenNonexistent() public { + uint256 tokenId = 999; + + vm.expectRevert(abi.encodeWithSelector(ERC721EnumerableFacet.ERC721NonexistentToken.selector, tokenId)); + harness.tokenURI(tokenId); + } + + // ============================================ + // Balance and Ownership Tests + // ============================================ + + function test_BalanceOf() public { + harness.mint(alice, 1); + harness.mint(alice, 2); + harness.mint(bob, 3); + + assertEq(harness.balanceOf(alice), 2); + assertEq(harness.balanceOf(bob), 1); + assertEq(harness.balanceOf(charlie), 0); + } + + function test_BalanceOfRevertWhenZeroAddress() public { + vm.expectRevert(abi.encodeWithSelector(ERC721EnumerableFacet.ERC721InvalidOwner.selector, address(0))); + harness.balanceOf(address(0)); + } + + function test_OwnerOf() public { + uint256 tokenId = 42; + harness.mint(alice, tokenId); + + assertEq(harness.ownerOf(tokenId), alice); + } + + function test_OwnerOfRevertWhenNonexistent() public { + uint256 tokenId = 999; + + vm.expectRevert(abi.encodeWithSelector(ERC721EnumerableFacet.ERC721NonexistentToken.selector, tokenId)); + harness.ownerOf(tokenId); + } + + // ============================================ + // Enumeration Tests + // ============================================ + + function test_TotalSupply() public { + assertEq(harness.totalSupply(), 0); + + harness.mint(alice, 1); + assertEq(harness.totalSupply(), 1); + + harness.mint(bob, 2); + assertEq(harness.totalSupply(), 2); + + harness.mint(charlie, 3); + assertEq(harness.totalSupply(), 3); + } + + function test_TokenOfOwnerByIndex() public { + harness.mint(alice, 10); + harness.mint(alice, 20); + harness.mint(alice, 30); + + assertEq(harness.tokenOfOwnerByIndex(alice, 0), 10); + assertEq(harness.tokenOfOwnerByIndex(alice, 1), 20); + assertEq(harness.tokenOfOwnerByIndex(alice, 2), 30); + } + + function test_TokenOfOwnerByIndexMultipleTokens() public { + harness.mint(alice, 1); + harness.mint(bob, 2); + harness.mint(alice, 3); + harness.mint(bob, 4); + + assertEq(harness.tokenOfOwnerByIndex(alice, 0), 1); + assertEq(harness.tokenOfOwnerByIndex(alice, 1), 3); + assertEq(harness.tokenOfOwnerByIndex(bob, 0), 2); + assertEq(harness.tokenOfOwnerByIndex(bob, 1), 4); + } + + function test_TokenOfOwnerByIndexRevertWhenOutOfBounds() public { + harness.mint(alice, 1); + + vm.expectRevert(abi.encodeWithSelector(ERC721EnumerableFacet.ERC721OutOfBoundsIndex.selector, alice, 1)); + harness.tokenOfOwnerByIndex(alice, 1); + } + + // ============================================ + // Approval Tests + // ============================================ + + function test_Approve() public { + uint256 tokenId = 1; + harness.mint(alice, tokenId); + + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit Approval(alice, bob, tokenId); + harness.approve(bob, tokenId); + + assertEq(harness.getApproved(tokenId), bob); + } + + function test_ApproveByOperator() public { + uint256 tokenId = 1; + harness.mint(alice, tokenId); + + vm.prank(alice); + harness.setApprovalForAll(charlie, true); + + vm.prank(charlie); + harness.approve(bob, tokenId); + + assertEq(harness.getApproved(tokenId), bob); + } + + function test_ApproveSelfApproval() public { + uint256 tokenId = 1; + harness.mint(alice, tokenId); + + vm.prank(alice); + harness.approve(alice, tokenId); + + assertEq(harness.getApproved(tokenId), alice); + } + + function test_ApproveClearsOnTransfer() public { + uint256 tokenId = 1; + harness.mint(alice, tokenId); + + vm.prank(alice); + harness.approve(bob, tokenId); + + vm.prank(alice); + harness.transferFrom(alice, charlie, tokenId); + + assertEq(harness.getApproved(tokenId), address(0)); + } + + function test_ApproveRevertWhenNonexistent() public { + uint256 tokenId = 999; + + vm.expectRevert(abi.encodeWithSelector(ERC721EnumerableFacet.ERC721NonexistentToken.selector, tokenId)); + vm.prank(alice); + harness.approve(bob, tokenId); + } + + function test_ApproveRevertWhenUnauthorized() public { + uint256 tokenId = 1; + harness.mint(alice, tokenId); + + vm.expectRevert(abi.encodeWithSelector(ERC721EnumerableFacet.ERC721InvalidApprover.selector, bob)); + vm.prank(bob); + harness.approve(charlie, tokenId); + } + + function test_GetApproved() public { + uint256 tokenId = 1; + harness.mint(alice, tokenId); + + assertEq(harness.getApproved(tokenId), address(0)); + + vm.prank(alice); + harness.approve(bob, tokenId); + + assertEq(harness.getApproved(tokenId), bob); + } + + function test_SetApprovalForAll() public { + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit ApprovalForAll(alice, bob, true); + harness.setApprovalForAll(bob, true); + + assertTrue(harness.isApprovedForAll(alice, bob)); + + vm.prank(alice); + harness.setApprovalForAll(bob, false); + + assertFalse(harness.isApprovedForAll(alice, bob)); + } + + function test_SetApprovalForAllRevertWhenZeroAddress() public { + vm.expectRevert(abi.encodeWithSelector(ERC721EnumerableFacet.ERC721InvalidOperator.selector, address(0))); + vm.prank(alice); + harness.setApprovalForAll(address(0), true); + } + + function test_IsApprovedForAll() public { + assertFalse(harness.isApprovedForAll(alice, bob)); + + vm.prank(alice); + harness.setApprovalForAll(bob, true); + + assertTrue(harness.isApprovedForAll(alice, bob)); + } + + // ============================================ + // Transfer Tests + // ============================================ + + function test_TransferFrom() public { + uint256 tokenId = 1; + harness.mint(alice, tokenId); + + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, bob, tokenId); + harness.transferFrom(alice, bob, tokenId); + + assertEq(harness.ownerOf(tokenId), bob); + assertEq(harness.balanceOf(alice), 0); + assertEq(harness.balanceOf(bob), 1); + } + + function test_TransferFromByApproved() public { + uint256 tokenId = 1; + harness.mint(alice, tokenId); + + vm.prank(alice); + harness.approve(bob, tokenId); + + vm.prank(bob); + harness.transferFrom(alice, charlie, tokenId); + + assertEq(harness.ownerOf(tokenId), charlie); + } + + function test_TransferFromByOperator() public { + uint256 tokenId = 1; + harness.mint(alice, tokenId); + + vm.prank(alice); + harness.setApprovalForAll(bob, true); + + vm.prank(bob); + harness.transferFrom(alice, charlie, tokenId); + + assertEq(harness.ownerOf(tokenId), charlie); + } + + function test_TransferFromToSelf() public { + uint256 tokenId = 1; + harness.mint(alice, tokenId); + + vm.prank(alice); + harness.transferFrom(alice, alice, tokenId); + + assertEq(harness.ownerOf(tokenId), alice); + assertEq(harness.balanceOf(alice), 1); + } + + function test_TransferFromUpdatesEnumeration() public { + harness.mint(alice, 1); + harness.mint(alice, 2); + harness.mint(alice, 3); + + vm.prank(alice); + harness.transferFrom(alice, bob, 2); + + // Alice should have tokens 1 and 3 + assertEq(harness.balanceOf(alice), 2); + assertEq(harness.tokenOfOwnerByIndex(alice, 0), 1); + assertEq(harness.tokenOfOwnerByIndex(alice, 1), 3); + + // Bob should have token 2 + assertEq(harness.balanceOf(bob), 1); + assertEq(harness.tokenOfOwnerByIndex(bob, 0), 2); + } + + function test_TransferFromRevertWhenNonexistent() public { + uint256 tokenId = 999; + + vm.expectRevert(abi.encodeWithSelector(ERC721EnumerableFacet.ERC721NonexistentToken.selector, tokenId)); + vm.prank(alice); + harness.transferFrom(alice, bob, tokenId); + } + + function test_TransferFromRevertWhenUnauthorized() public { + uint256 tokenId = 1; + harness.mint(alice, tokenId); + + vm.expectRevert(abi.encodeWithSelector(ERC721EnumerableFacet.ERC721InsufficientApproval.selector, bob, tokenId)); + vm.prank(bob); + harness.transferFrom(alice, charlie, tokenId); + } + + function test_TransferFromRevertWhenZeroAddress() public { + uint256 tokenId = 1; + harness.mint(alice, tokenId); + + vm.expectRevert(abi.encodeWithSelector(ERC721EnumerableFacet.ERC721InvalidReceiver.selector, address(0))); + vm.prank(alice); + harness.transferFrom(alice, address(0), tokenId); + } + + function test_TransferFromRevertWhenIncorrectOwner() public { + uint256 tokenId = 1; + harness.mint(alice, tokenId); + + vm.expectRevert( + abi.encodeWithSelector(ERC721EnumerableFacet.ERC721IncorrectOwner.selector, bob, tokenId, alice) + ); + vm.prank(alice); + harness.transferFrom(bob, charlie, tokenId); + } + + function test_SafeTransferFrom() public { + uint256 tokenId = 1; + harness.mint(alice, tokenId); + + vm.prank(alice); + harness.safeTransferFrom(alice, bob, tokenId); + + assertEq(harness.ownerOf(tokenId), bob); + } + + function test_SafeTransferFromWithData() public { + uint256 tokenId = 1; + harness.mint(alice, tokenId); + + vm.prank(alice); + harness.safeTransferFrom(alice, bob, tokenId, "test data"); + + assertEq(harness.ownerOf(tokenId), bob); + } + + function test_SafeTransferFromToEOA() public { + uint256 tokenId = 1; + harness.mint(alice, tokenId); + + vm.prank(alice); + harness.safeTransferFrom(alice, bob, tokenId); + + assertEq(harness.ownerOf(tokenId), bob); + } + + // ============================================ + // Fuzz Tests + // ============================================ + + function test_ApproveFuzz(address owner, address operator, uint256 tokenId) public { + vm.assume(owner != address(0)); + vm.assume(operator != address(0)); + vm.assume(tokenId < type(uint256).max); + + harness.mint(owner, tokenId); + + vm.prank(owner); + harness.approve(operator, tokenId); + + assertEq(harness.getApproved(tokenId), operator); + } + + function test_TransferFromFuzz(address from, address to, uint256 tokenId) public { + vm.assume(from != address(0)); + vm.assume(to != address(0)); + vm.assume(tokenId < type(uint256).max); + + harness.mint(from, tokenId); + + vm.prank(from); + harness.transferFrom(from, to, tokenId); + + assertEq(harness.ownerOf(tokenId), to); + } + + function test_SetApprovalForAllFuzz(address owner, address operator) public { + vm.assume(owner != address(0)); + vm.assume(operator != address(0)); + + vm.prank(owner); + harness.setApprovalForAll(operator, true); + + assertTrue(harness.isApprovedForAll(owner, operator)); + } +} diff --git a/test/token/ERC721/ERC721Enumerable/LibERC721Enumerable.t.sol b/test/token/ERC721/ERC721Enumerable/LibERC721Enumerable.t.sol new file mode 100644 index 00000000..8085b13f --- /dev/null +++ b/test/token/ERC721/ERC721Enumerable/LibERC721Enumerable.t.sol @@ -0,0 +1,628 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {LibERC721Enumerable} from "../../../../src/token/ERC721/ERC721Enumerable/LibERC721Enumerable.sol"; +import {LibERC721EnumerableHarness} from "./harnesses/LibERC721EnumerableHarness.sol"; + +contract LibERC721EnumerableTest is Test { + LibERC721EnumerableHarness public harness; + + address public alice; + address public bob; + address public charlie; + + string constant TOKEN_NAME = "Test Token"; + string constant TOKEN_SYMBOL = "TEST"; + string constant BASE_URI = "https://example.com/api/nft/"; + + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + + function setUp() public { + alice = makeAddr("alice"); + bob = makeAddr("bob"); + charlie = makeAddr("charlie"); + + harness = new LibERC721EnumerableHarness(); + harness.initialize(TOKEN_NAME, TOKEN_SYMBOL, BASE_URI); + } + + // ============================================ + // Metadata Tests + // ============================================ + + function test_Name() public view { + assertEq(harness.name(), TOKEN_NAME); + } + + function test_Symbol() public view { + assertEq(harness.symbol(), TOKEN_SYMBOL); + } + + function test_BaseURI() public view { + assertEq(harness.baseURI(), BASE_URI); + } + + // ============================================ + // Mint Tests + // ============================================ + + function test_Mint() public { + uint256 tokenId = 1; + + vm.expectEmit(true, true, true, true); + emit Transfer(address(0), alice, tokenId); + harness.mint(alice, tokenId); + + assertEq(harness.ownerOf(tokenId), alice); + } + + function test_MintUpdatesOwnership() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + assertEq(harness.ownerOf(tokenId), alice); + } + + function test_MintUpdatesBalance() public { + harness.mint(alice, 1); + assertEq(harness.balanceOf(alice), 1); + + harness.mint(alice, 2); + assertEq(harness.balanceOf(alice), 2); + + harness.mint(bob, 3); + assertEq(harness.balanceOf(bob), 1); + } + + function test_MintUpdatesOwnerTokens() public { + harness.mint(alice, 10); + harness.mint(alice, 20); + harness.mint(alice, 30); + + assertEq(harness.tokenOfOwnerByIndex(alice, 0), 10); + assertEq(harness.tokenOfOwnerByIndex(alice, 1), 20); + assertEq(harness.tokenOfOwnerByIndex(alice, 2), 30); + } + + function test_MintUpdatesAllTokens() public { + harness.mint(alice, 1); + harness.mint(bob, 2); + harness.mint(charlie, 3); + + assertEq(harness.totalSupply(), 3); + assertEq(harness.tokenByIndex(0), 1); + assertEq(harness.tokenByIndex(1), 2); + assertEq(harness.tokenByIndex(2), 3); + } + + function test_MintUpdatesIndices() public { + harness.mint(alice, 1); + harness.mint(alice, 2); + + // Verify tokens are at correct indices + assertEq(harness.tokenOfOwnerByIndex(alice, 0), 1); + assertEq(harness.tokenOfOwnerByIndex(alice, 1), 2); + } + + function test_MintMultipleTokens() public { + for (uint256 i = 1; i <= 10; i++) { + harness.mint(alice, i); + assertEq(harness.ownerOf(i), alice); + } + + assertEq(harness.balanceOf(alice), 10); + assertEq(harness.totalSupply(), 10); + } + + function test_MintToMultipleAddresses() public { + harness.mint(alice, 1); + harness.mint(bob, 2); + harness.mint(charlie, 3); + + assertEq(harness.ownerOf(1), alice); + assertEq(harness.ownerOf(2), bob); + assertEq(harness.ownerOf(3), charlie); + + assertEq(harness.balanceOf(alice), 1); + assertEq(harness.balanceOf(bob), 1); + assertEq(harness.balanceOf(charlie), 1); + } + + function test_MintEmitsTransferEvent() public { + uint256 tokenId = 1; + + vm.expectEmit(true, true, true, true); + emit Transfer(address(0), alice, tokenId); + harness.mint(alice, tokenId); + } + + function test_MintRevertWhenZeroAddress() public { + uint256 tokenId = 1; + + vm.expectRevert(abi.encodeWithSelector(LibERC721Enumerable.ERC721InvalidReceiver.selector, address(0))); + harness.mint(address(0), tokenId); + } + + function test_MintRevertWhenTokenExists() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + vm.expectRevert(abi.encodeWithSelector(LibERC721Enumerable.ERC721InvalidSender.selector, address(0))); + harness.mint(bob, tokenId); + } + + // ============================================ + // Transfer Tests + // ============================================ + + function test_TransferFrom() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, bob, tokenId); + harness.transferFrom(alice, bob, tokenId); + + assertEq(harness.ownerOf(tokenId), bob); + } + + function test_TransferFromByOwner() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + vm.prank(alice); + harness.transferFrom(alice, bob, tokenId); + + assertEq(harness.ownerOf(tokenId), bob); + } + + function test_TransferFromByApproved() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + vm.prank(alice); + harness.approve(bob, tokenId); + + vm.prank(bob); + harness.transferFrom(alice, charlie, tokenId); + + assertEq(harness.ownerOf(tokenId), charlie); + } + + function test_TransferFromByOperator() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + vm.prank(alice); + harness.setApprovalForAll(bob, true); + + vm.prank(bob); + harness.transferFrom(alice, charlie, tokenId); + + assertEq(harness.ownerOf(tokenId), charlie); + } + + function test_TransferFromUpdatesOwnership() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + vm.prank(alice); + harness.transferFrom(alice, bob, tokenId); + + assertEq(harness.ownerOf(tokenId), bob); + } + + function test_TransferFromUpdatesBalances() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + assertEq(harness.balanceOf(alice), 1); + assertEq(harness.balanceOf(bob), 0); + + vm.prank(alice); + harness.transferFrom(alice, bob, tokenId); + + assertEq(harness.balanceOf(alice), 0); + assertEq(harness.balanceOf(bob), 1); + } + + function test_TransferFromUpdatesOwnerTokens() public { + harness.mint(alice, 1); + harness.mint(alice, 2); + harness.mint(alice, 3); + + vm.prank(alice); + harness.transferFrom(alice, bob, 2); + + // Alice should have tokens 1 and 3 + assertEq(harness.balanceOf(alice), 2); + assertEq(harness.tokenOfOwnerByIndex(alice, 0), 1); + assertEq(harness.tokenOfOwnerByIndex(alice, 1), 3); + + // Bob should have token 2 + assertEq(harness.balanceOf(bob), 1); + assertEq(harness.tokenOfOwnerByIndex(bob, 0), 2); + } + + function test_TransferFromUpdatesIndices() public { + harness.mint(alice, 1); + harness.mint(alice, 2); + harness.mint(alice, 3); + + vm.prank(alice); + harness.transferFrom(alice, bob, 1); + + // Verify indices are correct after transfer + assertEq(harness.tokenOfOwnerByIndex(alice, 0), 3); + assertEq(harness.tokenOfOwnerByIndex(alice, 1), 2); + assertEq(harness.tokenOfOwnerByIndex(bob, 0), 1); + } + + function test_TransferFromClearsApproval() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + vm.prank(alice); + harness.approve(bob, tokenId); + + assertEq(harness.getApproved(tokenId), bob); + + vm.prank(alice); + harness.transferFrom(alice, charlie, tokenId); + + assertEq(harness.getApproved(tokenId), address(0)); + } + + function test_TransferFromEmitsTransferEvent() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, bob, tokenId); + harness.transferFrom(alice, bob, tokenId); + } + + function test_TransferFromRevertWhenNonexistent() public { + uint256 tokenId = 999; + + vm.expectRevert(abi.encodeWithSelector(LibERC721Enumerable.ERC721NonexistentToken.selector, tokenId)); + vm.prank(alice); + harness.transferFrom(alice, bob, tokenId); + } + + function test_TransferFromRevertWhenZeroAddress() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + vm.expectRevert(abi.encodeWithSelector(LibERC721Enumerable.ERC721InvalidReceiver.selector, address(0))); + vm.prank(alice); + harness.transferFrom(alice, address(0), tokenId); + } + + function test_TransferFromRevertWhenIncorrectOwner() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + vm.expectRevert(abi.encodeWithSelector(LibERC721Enumerable.ERC721IncorrectOwner.selector, bob, tokenId, alice)); + vm.prank(alice); + harness.transferFrom(bob, charlie, tokenId); + } + + function test_TransferFromRevertWhenUnauthorized() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + vm.expectRevert(abi.encodeWithSelector(LibERC721Enumerable.ERC721InsufficientApproval.selector, bob, tokenId)); + vm.prank(bob); + harness.transferFrom(alice, charlie, tokenId); + } + + // ============================================ + // Burn Tests + // ============================================ + + function test_Burn() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, address(0), tokenId); + harness.burn(tokenId); + + assertEq(harness.ownerOf(tokenId), address(0)); + } + + function test_BurnByOwner() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + vm.prank(alice); + harness.burn(tokenId); + + assertEq(harness.ownerOf(tokenId), address(0)); + } + + function test_BurnByApproved() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + vm.prank(alice); + harness.approve(bob, tokenId); + + vm.prank(bob); + harness.burn(tokenId); + + assertEq(harness.ownerOf(tokenId), address(0)); + } + + function test_BurnByOperator() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + vm.prank(alice); + harness.setApprovalForAll(bob, true); + + vm.prank(bob); + harness.burn(tokenId); + + assertEq(harness.ownerOf(tokenId), address(0)); + } + + function test_BurnUpdatesOwnership() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + assertEq(harness.ownerOf(tokenId), alice); + + vm.prank(alice); + harness.burn(tokenId); + + assertEq(harness.ownerOf(tokenId), address(0)); + } + + function test_BurnUpdatesBalance() public { + harness.mint(alice, 1); + harness.mint(alice, 2); + + assertEq(harness.balanceOf(alice), 2); + + vm.prank(alice); + harness.burn(1); + + assertEq(harness.balanceOf(alice), 1); + } + + function test_BurnUpdatesOwnerTokens() public { + harness.mint(alice, 1); + harness.mint(alice, 2); + harness.mint(alice, 3); + + vm.prank(alice); + harness.burn(2); + + // Alice should have tokens 1 and 3 + assertEq(harness.balanceOf(alice), 2); + assertEq(harness.tokenOfOwnerByIndex(alice, 0), 1); + assertEq(harness.tokenOfOwnerByIndex(alice, 1), 3); + } + + function test_BurnUpdatesAllTokens() public { + harness.mint(alice, 1); + harness.mint(bob, 2); + harness.mint(charlie, 3); + + assertEq(harness.totalSupply(), 3); + + vm.prank(bob); + harness.burn(2); + + assertEq(harness.totalSupply(), 2); + assertEq(harness.tokenByIndex(0), 1); + assertEq(harness.tokenByIndex(1), 3); + } + + function test_BurnUpdatesIndices() public { + harness.mint(alice, 1); + harness.mint(alice, 2); + harness.mint(alice, 3); + + vm.prank(alice); + harness.burn(1); + + // Verify indices are correct after burn + assertEq(harness.tokenOfOwnerByIndex(alice, 0), 3); + assertEq(harness.tokenOfOwnerByIndex(alice, 1), 2); + } + + function test_BurnClearsApproval() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + vm.prank(alice); + harness.approve(bob, tokenId); + + assertEq(harness.getApproved(tokenId), bob); + + vm.prank(alice); + harness.burn(tokenId); + + assertEq(harness.getApproved(tokenId), address(0)); + } + + function test_BurnEmitsTransferEvent() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, address(0), tokenId); + harness.burn(tokenId); + } + + function test_BurnRevertWhenNonexistent() public { + uint256 tokenId = 999; + + vm.expectRevert(abi.encodeWithSelector(LibERC721Enumerable.ERC721NonexistentToken.selector, tokenId)); + vm.prank(alice); + harness.burn(tokenId); + } + + function test_BurnRevertWhenUnauthorized() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + + vm.expectRevert(abi.encodeWithSelector(LibERC721Enumerable.ERC721InsufficientApproval.selector, bob, tokenId)); + vm.prank(bob); + harness.burn(tokenId); + } + + // ============================================ + // Enumeration Tests + // ============================================ + + function test_EnumerationAfterMultipleMints() public { + harness.mint(alice, 1); + harness.mint(bob, 2); + harness.mint(alice, 3); + harness.mint(charlie, 4); + harness.mint(bob, 5); + + assertEq(harness.totalSupply(), 5); + assertEq(harness.balanceOf(alice), 2); + assertEq(harness.balanceOf(bob), 2); + assertEq(harness.balanceOf(charlie), 1); + + assertEq(harness.tokenOfOwnerByIndex(alice, 0), 1); + assertEq(harness.tokenOfOwnerByIndex(alice, 1), 3); + assertEq(harness.tokenOfOwnerByIndex(bob, 0), 2); + assertEq(harness.tokenOfOwnerByIndex(bob, 1), 5); + assertEq(harness.tokenOfOwnerByIndex(charlie, 0), 4); + } + + function test_EnumerationAfterTransfers() public { + harness.mint(alice, 1); + harness.mint(alice, 2); + harness.mint(alice, 3); + + vm.prank(alice); + harness.transferFrom(alice, bob, 2); + + assertEq(harness.balanceOf(alice), 2); + assertEq(harness.balanceOf(bob), 1); + + assertEq(harness.tokenOfOwnerByIndex(alice, 0), 1); + assertEq(harness.tokenOfOwnerByIndex(alice, 1), 3); + assertEq(harness.tokenOfOwnerByIndex(bob, 0), 2); + } + + function test_EnumerationAfterBurns() public { + harness.mint(alice, 1); + harness.mint(alice, 2); + harness.mint(alice, 3); + + vm.prank(alice); + harness.burn(2); + + assertEq(harness.totalSupply(), 2); + assertEq(harness.balanceOf(alice), 2); + + assertEq(harness.tokenOfOwnerByIndex(alice, 0), 1); + assertEq(harness.tokenOfOwnerByIndex(alice, 1), 3); + assertEq(harness.tokenByIndex(0), 1); + assertEq(harness.tokenByIndex(1), 3); + } + + function test_EnumerationComplexScenario() public { + // Mint tokens + harness.mint(alice, 1); + harness.mint(alice, 2); + harness.mint(bob, 3); + harness.mint(charlie, 4); + + assertEq(harness.totalSupply(), 4); + + // Transfer token + vm.prank(alice); + harness.transferFrom(alice, bob, 1); + + assertEq(harness.balanceOf(alice), 1); + assertEq(harness.balanceOf(bob), 2); + + // Burn token + vm.prank(charlie); + harness.burn(4); + + assertEq(harness.totalSupply(), 3); + assertEq(harness.balanceOf(charlie), 0); + + // Verify final state + assertEq(harness.tokenOfOwnerByIndex(alice, 0), 2); + assertEq(harness.tokenOfOwnerByIndex(bob, 0), 3); + assertEq(harness.tokenOfOwnerByIndex(bob, 1), 1); + } + + // ============================================ + // Fuzz Tests + // ============================================ + + function test_MintFuzz(address to, uint256 tokenId) public { + vm.assume(to != address(0)); + vm.assume(tokenId < type(uint256).max); + + harness.mint(to, tokenId); + + assertEq(harness.ownerOf(tokenId), to); + assertEq(harness.balanceOf(to), 1); + assertEq(harness.totalSupply(), 1); + } + + function test_TransferFromFuzz(address from, address to, uint256 tokenId) public { + vm.assume(from != address(0)); + vm.assume(to != address(0)); + vm.assume(tokenId < type(uint256).max); + + harness.mint(from, tokenId); + + vm.prank(from); + harness.transferFrom(from, to, tokenId); + + assertEq(harness.ownerOf(tokenId), to); + } + + function test_BurnFuzz(address to, uint256 tokenId) public { + vm.assume(to != address(0)); + vm.assume(tokenId < type(uint256).max); + + harness.mint(to, tokenId); + assertEq(harness.ownerOf(tokenId), to); + + vm.prank(to); + harness.burn(tokenId); + + assertEq(harness.ownerOf(tokenId), address(0)); + assertEq(harness.balanceOf(to), 0); + } +} diff --git a/test/token/ERC721/ERC721Enumerable/harnesses/ERC721EnumerableFacetHarness.sol b/test/token/ERC721/ERC721Enumerable/harnesses/ERC721EnumerableFacetHarness.sol new file mode 100644 index 00000000..3a78f77c --- /dev/null +++ b/test/token/ERC721/ERC721Enumerable/harnesses/ERC721EnumerableFacetHarness.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {ERC721EnumerableFacet} from "../../../../../src/token/ERC721/ERC721Enumerable/ERC721EnumerableFacet.sol"; + +/// @title ERC721EnumerableFacetHarness +/// @notice Test harness for ERC721EnumerableFacet that adds initialization and minting helpers +contract ERC721EnumerableFacetHarness is ERC721EnumerableFacet { + /// @notice Initialize the ERC721 enumerable token storage + /// @dev Only used for testing - production diamonds should initialize in constructor + function initialize(string memory _name, string memory _symbol, string memory _baseURI) external { + ERC721EnumerableStorage storage s = getStorage(); + s.name = _name; + s.symbol = _symbol; + s.baseURI = _baseURI; + } + + /// @notice Mints a new ERC-721 token to the specified address. + /// @dev Reverts if the receiver address is zero or if the token already exists. + /// @param _to The address that will own the newly minted token. + /// @param _tokenId The ID of the token to mint. + function mint(address _to, uint256 _tokenId) external { + ERC721EnumerableStorage storage s = getStorage(); + if (_to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + if (s.ownerOf[_tokenId] != address(0)) { + revert ERC721InvalidSender(address(0)); + } + + s.ownerOf[_tokenId] = _to; + s.ownerTokensIndex[_tokenId] = s.ownerTokens[_to].length; + s.ownerTokens[_to].push(_tokenId); + s.allTokensIndex[_tokenId] = s.allTokens.length; + s.allTokens.push(_tokenId); + emit Transfer(address(0), _to, _tokenId); + } +} diff --git a/test/token/ERC721/ERC721Enumerable/harnesses/LibERC721EnumerableHarness.sol b/test/token/ERC721/ERC721Enumerable/harnesses/LibERC721EnumerableHarness.sol new file mode 100644 index 00000000..74e865fe --- /dev/null +++ b/test/token/ERC721/ERC721Enumerable/harnesses/LibERC721EnumerableHarness.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {LibERC721Enumerable} from "../../../../../src/token/ERC721/ERC721Enumerable/LibERC721Enumerable.sol"; + +/// @title LibERC721EnumerableHarness +/// @notice Test harness that exposes LibERC721Enumerable library functions as external functions +contract LibERC721EnumerableHarness { + /// @notice Initialize the ERC721 enumerable token storage + /// @dev Only used for testing + function initialize(string memory _name, string memory _symbol, string memory _baseURI) external { + LibERC721Enumerable.ERC721EnumerableStorage storage s = LibERC721Enumerable.getStorage(); + s.name = _name; + s.symbol = _symbol; + s.baseURI = _baseURI; + } + + /// @notice Exposes LibERC721Enumerable.mint as an external function + function mint(address _to, uint256 _tokenId) external { + LibERC721Enumerable.mint(_to, _tokenId); + } + + /// @notice Exposes LibERC721Enumerable.burn as an external function + function burn(uint256 _tokenId) external { + LibERC721Enumerable.burn(_tokenId, msg.sender); + } + + /// @notice Exposes LibERC721Enumerable.transferFrom as an external function + function transferFrom(address _from, address _to, uint256 _tokenId) external { + LibERC721Enumerable.transferFrom(_from, _to, _tokenId, msg.sender); + } + + /// @notice Expose owner lookup for a given token id + function ownerOf(uint256 _tokenId) external view returns (address) { + return LibERC721Enumerable.getStorage().ownerOf[_tokenId]; + } + + /// @notice Get balance of an address + function balanceOf(address _owner) external view returns (uint256) { + return LibERC721Enumerable.getStorage().ownerTokens[_owner].length; + } + + /// @notice Get total supply + function totalSupply() external view returns (uint256) { + return LibERC721Enumerable.getStorage().allTokens.length; + } + + /// @notice Get token by index in owner's list + function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256) { + return LibERC721Enumerable.getStorage().ownerTokens[_owner][_index]; + } + + /// @notice Get token by global index + function tokenByIndex(uint256 _index) external view returns (uint256) { + return LibERC721Enumerable.getStorage().allTokens[_index]; + } + + /// @notice Get storage values for testing + function name() external view returns (string memory) { + return LibERC721Enumerable.getStorage().name; + } + + function symbol() external view returns (string memory) { + return LibERC721Enumerable.getStorage().symbol; + } + + function baseURI() external view returns (string memory) { + return LibERC721Enumerable.getStorage().baseURI; + } + + /// @notice Set approval for a token + function approve(address _approved, uint256 _tokenId) external { + LibERC721Enumerable.ERC721EnumerableStorage storage s = LibERC721Enumerable.getStorage(); + s.approved[_tokenId] = _approved; + } + + /// @notice Get approved address for a token + function getApproved(uint256 _tokenId) external view returns (address) { + return LibERC721Enumerable.getStorage().approved[_tokenId]; + } + + /// @notice Set approval for all tokens + function setApprovalForAll(address _operator, bool _approved) external { + LibERC721Enumerable.getStorage().isApprovedForAll[msg.sender][_operator] = _approved; + } + + /// @notice Check if operator is approved for all + function isApprovedForAll(address _owner, address _operator) external view returns (bool) { + return LibERC721Enumerable.getStorage().isApprovedForAll[_owner][_operator]; + } +}