diff --git a/src/token/ERC721/ERC721Enumerable/LibERC721Enumerable.sol b/src/token/ERC721/ERC721Enumerable/LibERC721Enumerable.sol index f7a467b8..f381fdd1 100644 --- a/src/token/ERC721/ERC721Enumerable/LibERC721Enumerable.sol +++ b/src/token/ERC721/ERC721Enumerable/LibERC721Enumerable.sol @@ -123,6 +123,7 @@ library LibERC721 { s.ownedTokensOf[_to].push(_tokenId); s.allTokensIndexOf[_tokenId] = s.allTokens.length; s.allTokens.push(_tokenId); + s.ownerOf[_tokenId] = _to; emit Transfer(address(0), _to, _tokenId); } diff --git a/test/token/ERC721/ERC721Enumerable/ERC721EnumerableFacet.t.sol b/test/token/ERC721/ERC721Enumerable/ERC721EnumerableFacet.t.sol new file mode 100644 index 00000000..6f23cc3a --- /dev/null +++ b/test/token/ERC721/ERC721Enumerable/ERC721EnumerableFacet.t.sol @@ -0,0 +1,471 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {ERC721EnumerableFacetHarness} from "./harnesses/ERC721EnumerableFacetHarness.sol"; + +contract ERC721EnumerableFacetTest is Test { + ERC721EnumerableFacetHarness public facet; + + address public alice; + address public bob; + address public charlie; + address public operator; + + string constant TOKEN_NAME = "Test NFT"; + string constant TOKEN_SYMBOL = "TNFT"; + string constant BASE_URI = "https://example.com/token/"; + + 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"); + operator = makeAddr("operator"); + + facet = new ERC721EnumerableFacetHarness(); + facet.initialize(TOKEN_NAME, TOKEN_SYMBOL, BASE_URI); + } + + // ============================================ + // Metadata Tests + // ============================================ + + function test_Name() public view { + assertEq(facet.name(), TOKEN_NAME); + } + + function test_Symbol() public view { + assertEq(facet.symbol(), TOKEN_SYMBOL); + } + + function test_TokenURI() public { + facet.mint(alice, 1); + assertEq(facet.tokenURI(1), "https://example.com/token/1"); + } + + function test_TokenURI_Zero() public { + facet.mint(alice, 0); + assertEq(facet.tokenURI(0), "https://example.com/token/0"); + } + + function test_TokenURI_LargeNumber() public { + uint256 tokenId = 123456789; + facet.mint(alice, tokenId); + assertEq(facet.tokenURI(tokenId), "https://example.com/token/123456789"); + } + + function test_RevertWhen_TokenURINonexistentToken() public { + vm.expectRevert(); + facet.tokenURI(999); + } + + // ============================================ + // Balance and Ownership Tests + // ============================================ + + function test_BalanceOf() public { + facet.mint(alice, 1); + facet.mint(alice, 2); + facet.mint(bob, 3); + + assertEq(facet.balanceOf(alice), 2); + assertEq(facet.balanceOf(bob), 1); + assertEq(facet.balanceOf(charlie), 0); + } + + function test_RevertWhen_BalanceOfZeroAddress() public { + vm.expectRevert(); + facet.balanceOf(address(0)); + } + + function test_OwnerOf() public { + facet.mint(alice, 1); + assertEq(facet.ownerOf(1), alice); + } + + function test_RevertWhen_OwnerOfNonexistentToken() public { + vm.expectRevert(); + facet.ownerOf(999); + } + + // ============================================ + // Enumeration Tests + // ============================================ + + function test_TotalSupply() public { + assertEq(facet.totalSupply(), 0); + + facet.mint(alice, 1); + assertEq(facet.totalSupply(), 1); + + facet.mint(bob, 2); + assertEq(facet.totalSupply(), 2); + + vm.prank(alice); + facet.burn(1); + assertEq(facet.totalSupply(), 1); + } + + function test_TokenOfOwnerByIndex() public { + facet.mint(alice, 10); + facet.mint(alice, 20); + facet.mint(alice, 30); + + assertEq(facet.tokenOfOwnerByIndex(alice, 0), 10); + assertEq(facet.tokenOfOwnerByIndex(alice, 1), 20); + assertEq(facet.tokenOfOwnerByIndex(alice, 2), 30); + } + + function test_RevertWhen_TokenOfOwnerByIndexOutOfBounds() public { + facet.mint(alice, 1); + + vm.expectRevert(); + facet.tokenOfOwnerByIndex(alice, 1); // Index 1 is out of bounds + } + + // ============================================ + // Approval Tests + // ============================================ + + function test_Approve() public { + facet.mint(alice, 1); + + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit Approval(alice, bob, 1); + facet.approve(bob, 1); + + assertEq(facet.getApproved(1), bob); + } + + function test_Approve_OwnerCanApprove() public { + facet.mint(alice, 1); + + vm.prank(alice); + facet.approve(bob, 1); + + assertEq(facet.getApproved(1), bob); + } + + function test_Approve_OperatorCanApprove() public { + facet.mint(alice, 1); + + vm.prank(alice); + facet.setApprovalForAll(operator, true); + + vm.prank(operator); + facet.approve(bob, 1); + + assertEq(facet.getApproved(1), bob); + } + + function test_RevertWhen_ApproveNonexistentToken() public { + vm.prank(alice); + vm.expectRevert(); + facet.approve(bob, 999); + } + + function test_RevertWhen_ApproveUnauthorized() public { + facet.mint(alice, 1); + + vm.prank(bob); + vm.expectRevert(); + facet.approve(charlie, 1); + } + + function test_SetApprovalForAll() public { + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit ApprovalForAll(alice, operator, true); + facet.setApprovalForAll(operator, true); + + assertTrue(facet.isApprovedForAll(alice, operator)); + } + + function test_SetApprovalForAll_Revoke() public { + vm.startPrank(alice); + facet.setApprovalForAll(operator, true); + facet.setApprovalForAll(operator, false); + vm.stopPrank(); + + assertFalse(facet.isApprovedForAll(alice, operator)); + } + + function test_RevertWhen_SetApprovalForAllZeroAddress() public { + vm.prank(alice); + vm.expectRevert(); + facet.setApprovalForAll(address(0), true); + } + + function test_GetApproved() public { + facet.mint(alice, 1); + + vm.prank(alice); + facet.approve(bob, 1); + + assertEq(facet.getApproved(1), bob); + } + + function test_RevertWhen_GetApprovedNonexistentToken() public { + vm.expectRevert(); + facet.getApproved(999); + } + + function test_IsApprovedForAll() public { + vm.prank(alice); + facet.setApprovalForAll(operator, true); + + assertTrue(facet.isApprovedForAll(alice, operator)); + assertFalse(facet.isApprovedForAll(alice, bob)); + } + + // ============================================ + // Transfer Tests + // ============================================ + + function test_TransferFrom_ByOwner() public { + facet.mint(alice, 1); + + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, bob, 1); + facet.transferFrom(alice, bob, 1); + + assertEq(facet.ownerOf(1), bob); + assertEq(facet.balanceOf(alice), 0); + assertEq(facet.balanceOf(bob), 1); + } + + function test_TransferFrom_ByApprovedAddress() public { + facet.mint(alice, 1); + + vm.prank(alice); + facet.approve(bob, 1); + + vm.prank(bob); + facet.transferFrom(alice, charlie, 1); + + assertEq(facet.ownerOf(1), charlie); + } + + function test_TransferFrom_ByOperator() public { + facet.mint(alice, 1); + + vm.prank(alice); + facet.setApprovalForAll(operator, true); + + vm.prank(operator); + facet.transferFrom(alice, bob, 1); + + assertEq(facet.ownerOf(1), bob); + } + + function test_TransferFrom_ClearsApproval() public { + facet.mint(alice, 1); + + vm.prank(alice); + facet.approve(bob, 1); + + vm.prank(alice); + facet.transferFrom(alice, charlie, 1); + + assertEq(facet.getApproved(1), address(0)); + } + + function test_RevertWhen_TransferFromIncorrectOwner() public { + facet.mint(alice, 1); + + vm.prank(alice); + vm.expectRevert(); + facet.transferFrom(bob, charlie, 1); + } + + function test_RevertWhen_TransferFromToZeroAddress() public { + facet.mint(alice, 1); + + vm.prank(alice); + vm.expectRevert(); + facet.transferFrom(alice, address(0), 1); + } + + function test_RevertWhen_TransferFromUnauthorized() public { + facet.mint(alice, 1); + + vm.prank(bob); + vm.expectRevert(); + facet.transferFrom(alice, bob, 1); + } + + // ============================================ + // SafeTransferFrom Tests + // ============================================ + + function test_SafeTransferFrom_ToEOA() public { + facet.mint(alice, 1); + + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, bob, 1); + facet.safeTransferFrom(alice, bob, 1); + + assertEq(facet.ownerOf(1), bob); + } + + function test_SafeTransferFrom_WithData() public { + facet.mint(alice, 1); + bytes memory data = "test data"; + + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, bob, 1); + facet.safeTransferFrom(alice, bob, 1, data); + + assertEq(facet.ownerOf(1), bob); + } + + function test_SafeTransferFrom_ByOperator() public { + facet.mint(alice, 1); + + vm.prank(alice); + facet.setApprovalForAll(operator, true); + + vm.prank(operator); + facet.safeTransferFrom(alice, bob, 1); + + assertEq(facet.ownerOf(1), bob); + } + + // ============================================ + // Integration Tests + // ============================================ + + function test_MintApproveTransfer_Flow() public { + // Mint token to alice + facet.mint(alice, 1); + assertEq(facet.ownerOf(1), alice); + + // Alice approves bob + vm.prank(alice); + facet.approve(bob, 1); + assertEq(facet.getApproved(1), bob); + + // Bob transfers to charlie + vm.prank(bob); + facet.transferFrom(alice, charlie, 1); + assertEq(facet.ownerOf(1), charlie); + + // Approval should be cleared + assertEq(facet.getApproved(1), address(0)); + } + + function test_MintSetOperatorTransfer_Flow() public { + // Mint token to alice + facet.mint(alice, 1); + + // Alice sets operator + vm.prank(alice); + facet.setApprovalForAll(operator, true); + + // Operator transfers + vm.prank(operator); + facet.transferFrom(alice, bob, 1); + + assertEq(facet.ownerOf(1), bob); + } + + function test_MultipleTokensEnumeration() public { + // Mint multiple tokens + facet.mint(alice, 1); + facet.mint(alice, 2); + facet.mint(bob, 3); + facet.mint(bob, 4); + facet.mint(charlie, 5); + + // Check total supply + assertEq(facet.totalSupply(), 5); + + // Check balances + assertEq(facet.balanceOf(alice), 2); + assertEq(facet.balanceOf(bob), 2); + assertEq(facet.balanceOf(charlie), 1); + + // Check enumeration + assertEq(facet.tokenOfOwnerByIndex(alice, 0), 1); + assertEq(facet.tokenOfOwnerByIndex(alice, 1), 2); + assertEq(facet.tokenOfOwnerByIndex(bob, 0), 3); + assertEq(facet.tokenOfOwnerByIndex(bob, 1), 4); + assertEq(facet.tokenOfOwnerByIndex(charlie, 0), 5); + + // Transfer one token + vm.prank(alice); + facet.transferFrom(alice, bob, 1); + + // Check updated state + assertEq(facet.balanceOf(alice), 1); + assertEq(facet.balanceOf(bob), 3); + assertEq(facet.ownerOf(1), bob); + } + + function test_BurnAndEnumeration() public { + // Mint tokens + facet.mint(alice, 1); + facet.mint(alice, 2); + facet.mint(alice, 3); + + assertEq(facet.totalSupply(), 3); + assertEq(facet.balanceOf(alice), 3); + + // Burn middle token + vm.prank(alice); + facet.burn(2); + + // Check updated state + assertEq(facet.totalSupply(), 2); + assertEq(facet.balanceOf(alice), 2); + + // Check remaining tokens + assertEq(facet.ownerOf(1), alice); + assertEq(facet.ownerOf(3), alice); + } + + // ============================================ + // Edge Cases + // ============================================ + + function test_ApprovalPersistsAcrossQueries() public { + facet.mint(alice, 1); + + vm.prank(alice); + facet.approve(bob, 1); + + assertEq(facet.getApproved(1), bob); + assertEq(facet.getApproved(1), bob); // Should still be bob + } + + function test_OperatorApprovalPersists() public { + vm.prank(alice); + facet.setApprovalForAll(operator, true); + + assertTrue(facet.isApprovedForAll(alice, operator)); + assertTrue(facet.isApprovedForAll(alice, operator)); // Should still be true + } + + function test_ZeroBalanceOwner() public view { + assertEq(facet.balanceOf(alice), 0); + } + + function testFuzz_TokenURI(uint256 tokenId) public { + vm.assume(tokenId > 0 && tokenId < type(uint128).max); + + facet.mint(alice, tokenId); + + string memory uri = facet.tokenURI(tokenId); + // URI should contain the base URI + assertTrue(bytes(uri).length > 0); + } +} + diff --git a/test/token/ERC721/ERC721Enumerable/LibERC721Enumerable.t.sol b/test/token/ERC721/ERC721Enumerable/LibERC721Enumerable.t.sol new file mode 100644 index 00000000..baef8658 --- /dev/null +++ b/test/token/ERC721/ERC721Enumerable/LibERC721Enumerable.t.sol @@ -0,0 +1,406 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {LibERC721EnumerableHarness} from "./harnesses/LibERC721EnumerableHarness.sol"; +import {LibERC721} from "../../../../src/token/ERC721/ERC721Enumerable/LibERC721Enumerable.sol"; + +contract LibERC721EnumerableTest is Test { + LibERC721EnumerableHarness public harness; + + address public alice; + address public bob; + address public charlie; + + string constant TOKEN_NAME = "Test NFT"; + string constant TOKEN_SYMBOL = "TNFT"; + string constant BASE_URI = "https://example.com/token/"; + + 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); + } + + function test_InitialTotalSupply() public view { + assertEq(harness.totalSupply(), 0); + } + + // ============================================ + // Mint Tests - CORE BUG FIX VALIDATION + // ============================================ + + function test_Mint_SetsOwner() public { + uint256 tokenId = 1; + + vm.expectEmit(true, true, true, true); + emit Transfer(address(0), alice, tokenId); + harness.mint(alice, tokenId); + + // CRITICAL: This is the bug we're fixing - ownerOf must be set + assertEq(harness.ownerOf(tokenId), alice, "Owner not set correctly"); + assertEq(harness.balanceOf(alice), 1); + assertEq(harness.totalSupply(), 1); + } + + function test_Mint_Multiple_SetsOwnersCorrectly() public { + harness.mint(alice, 1); + harness.mint(bob, 2); + harness.mint(alice, 3); + + // Verify each token has correct owner + assertEq(harness.ownerOf(1), alice, "Token 1 owner incorrect"); + assertEq(harness.ownerOf(2), bob, "Token 2 owner incorrect"); + assertEq(harness.ownerOf(3), alice, "Token 3 owner incorrect"); + + assertEq(harness.balanceOf(alice), 2); + assertEq(harness.balanceOf(bob), 1); + assertEq(harness.totalSupply(), 3); + } + + function test_Mint_UpdatesOwnedTokens() public { + harness.mint(alice, 1); + harness.mint(alice, 2); + harness.mint(bob, 3); + + assertEq(harness.tokenOfOwnerByIndex(alice, 0), 1); + assertEq(harness.tokenOfOwnerByIndex(alice, 1), 2); + assertEq(harness.tokenOfOwnerByIndex(bob, 0), 3); + } + + function test_Mint_UpdatesAllTokens() public { + harness.mint(alice, 10); + harness.mint(bob, 20); + harness.mint(charlie, 30); + + assertEq(harness.totalSupply(), 3); + assertEq(harness.tokenByIndex(0), 10); + assertEq(harness.tokenByIndex(1), 20); + assertEq(harness.tokenByIndex(2), 30); + } + + function test_Mint_UpdatesIndices() public { + harness.mint(alice, 1); + harness.mint(alice, 2); + + assertEq(harness.ownedTokensIndexOf(1), 0); + assertEq(harness.ownedTokensIndexOf(2), 1); + assertEq(harness.allTokensIndexOf(1), 0); + assertEq(harness.allTokensIndexOf(2), 1); + } + + function testFuzz_Mint_SetsOwner(address to, uint256 tokenId) public { + vm.assume(to != address(0)); + vm.assume(tokenId > 0 && tokenId < type(uint256).max); + + harness.mint(to, tokenId); + + // The critical assertion - owner must be set + assertEq(harness.ownerOf(tokenId), to); + assertEq(harness.balanceOf(to), 1); + assertEq(harness.totalSupply(), 1); + } + + function test_RevertWhen_MintToZeroAddress() public { + vm.expectRevert(abi.encodeWithSelector(LibERC721.ERC721InvalidReceiver.selector, address(0))); + harness.mint(address(0), 1); + } + + function test_RevertWhen_MintExistingToken() public { + harness.mint(alice, 1); + + vm.expectRevert(abi.encodeWithSelector(LibERC721.ERC721InvalidSender.selector, address(0))); + harness.mint(bob, 1); + } + + // ============================================ + // Burn Tests + // ============================================ + + function test_Burn() public { + harness.mint(alice, 1); + assertEq(harness.ownerOf(1), alice); + + vm.expectEmit(true, true, true, true); + emit Transfer(alice, address(0), 1); + harness.burn(1, alice); + + assertEq(harness.ownerOf(1), address(0)); + assertEq(harness.balanceOf(alice), 0); + assertEq(harness.totalSupply(), 0); + } + + function test_Burn_RemovesFromOwnedTokens() public { + harness.mint(alice, 1); + harness.mint(alice, 2); + harness.mint(alice, 3); + + assertEq(harness.balanceOf(alice), 3); + + harness.burn(2, alice); + + assertEq(harness.balanceOf(alice), 2); + assertEq(harness.tokenOfOwnerByIndex(alice, 0), 1); + assertEq(harness.tokenOfOwnerByIndex(alice, 1), 3); // Last token moved to index 1 + } + + function test_Burn_RemovesFromAllTokens() public { + harness.mint(alice, 1); + harness.mint(bob, 2); + harness.mint(charlie, 3); + + assertEq(harness.totalSupply(), 3); + + harness.burn(2, bob); + + assertEq(harness.totalSupply(), 2); + assertEq(harness.tokenByIndex(0), 1); + assertEq(harness.tokenByIndex(1), 3); // Last token moved to index 1 + } + + function test_Burn_LastToken() public { + harness.mint(alice, 1); + harness.mint(alice, 2); + + harness.burn(2, alice); + + assertEq(harness.balanceOf(alice), 1); + assertEq(harness.totalSupply(), 1); + assertEq(harness.tokenOfOwnerByIndex(alice, 0), 1); + } + + function test_RevertWhen_BurnNonexistentToken() public { + vm.expectRevert(abi.encodeWithSelector(LibERC721.ERC721NonexistentToken.selector, 1)); + harness.burn(1, alice); + } + + function test_RevertWhen_BurnUnauthorized() public { + harness.mint(alice, 1); + + vm.expectRevert(abi.encodeWithSelector(LibERC721.ERC721InsufficientApproval.selector, bob, 1)); + harness.burn(1, bob); + } + + // ============================================ + // Transfer Tests + // ============================================ + + function test_TransferFrom() public { + harness.mint(alice, 1); + + vm.expectEmit(true, true, true, true); + emit Transfer(alice, bob, 1); + harness.transferFrom(alice, bob, 1, alice); + + assertEq(harness.ownerOf(1), bob); + assertEq(harness.balanceOf(alice), 0); + assertEq(harness.balanceOf(bob), 1); + } + + function test_TransferFrom_UpdatesOwnedTokens() public { + harness.mint(alice, 1); + harness.mint(alice, 2); + harness.mint(alice, 3); + + harness.transferFrom(alice, bob, 2, alice); + + // 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_TransferFrom_DoesNotAffectAllTokens() public { + harness.mint(alice, 1); + harness.mint(bob, 2); + + harness.transferFrom(alice, charlie, 1, alice); + + // Total supply and allTokens should remain the same + assertEq(harness.totalSupply(), 2); + assertEq(harness.tokenByIndex(0), 1); + assertEq(harness.tokenByIndex(1), 2); + } + + function test_TransferFrom_MultipleTransfers() public { + harness.mint(alice, 1); + + harness.transferFrom(alice, bob, 1, alice); + assertEq(harness.ownerOf(1), bob); + + harness.transferFrom(bob, charlie, 1, bob); + assertEq(harness.ownerOf(1), charlie); + + assertEq(harness.balanceOf(alice), 0); + assertEq(harness.balanceOf(bob), 0); + assertEq(harness.balanceOf(charlie), 1); + } + + function testFuzz_TransferFrom(address from, address to, uint256 tokenId) public { + vm.assume(from != address(0) && to != address(0)); + vm.assume(from != to); + vm.assume(tokenId > 0); + + harness.mint(from, tokenId); + + harness.transferFrom(from, to, tokenId, from); + + assertEq(harness.ownerOf(tokenId), to); + assertEq(harness.balanceOf(from), 0); + assertEq(harness.balanceOf(to), 1); + } + + function test_RevertWhen_TransferFromNonexistentToken() public { + vm.expectRevert(abi.encodeWithSelector(LibERC721.ERC721NonexistentToken.selector, 999)); + harness.transferFrom(alice, bob, 999, alice); + } + + function test_RevertWhen_TransferFromIncorrectOwner() public { + harness.mint(alice, 1); + + vm.expectRevert(abi.encodeWithSelector(LibERC721.ERC721IncorrectOwner.selector, bob, 1, alice)); + harness.transferFrom(bob, charlie, 1, bob); + } + + function test_RevertWhen_TransferFromToZeroAddress() public { + harness.mint(alice, 1); + + vm.expectRevert(abi.encodeWithSelector(LibERC721.ERC721InvalidReceiver.selector, address(0))); + harness.transferFrom(alice, address(0), 1, alice); + } + + function test_RevertWhen_TransferFromUnauthorized() public { + harness.mint(alice, 1); + + vm.expectRevert(abi.encodeWithSelector(LibERC721.ERC721InsufficientApproval.selector, bob, 1)); + harness.transferFrom(alice, charlie, 1, bob); + } + + // ============================================ + // Integration Tests + // ============================================ + + function test_MintTransferBurn_Flow() public { + // Mint multiple tokens + harness.mint(alice, 1); + harness.mint(alice, 2); + harness.mint(bob, 3); + + assertEq(harness.totalSupply(), 3); + assertEq(harness.balanceOf(alice), 2); + assertEq(harness.balanceOf(bob), 1); + + // Transfer + harness.transferFrom(alice, charlie, 1, alice); + + assertEq(harness.ownerOf(1), charlie); + assertEq(harness.balanceOf(alice), 1); + assertEq(harness.balanceOf(charlie), 1); + + // Burn + harness.burn(2, alice); + + assertEq(harness.totalSupply(), 2); + assertEq(harness.balanceOf(alice), 0); + + // Final state + assertEq(harness.ownerOf(1), charlie); + assertEq(harness.ownerOf(3), bob); + assertEq(harness.totalSupply(), 2); + } + + function test_ComplexEnumeration_Flow() public { + // Mint tokens to multiple addresses + harness.mint(alice, 1); + harness.mint(alice, 2); + harness.mint(alice, 3); + harness.mint(bob, 4); + harness.mint(bob, 5); + + // Verify enumeration + assertEq(harness.totalSupply(), 5); + assertEq(harness.balanceOf(alice), 3); + assertEq(harness.balanceOf(bob), 2); + + // Transfer one from alice to bob + harness.transferFrom(alice, bob, 2, alice); + + // Verify updated enumeration + assertEq(harness.balanceOf(alice), 2); + assertEq(harness.balanceOf(bob), 3); + + // Burn from middle of bob's collection + harness.burn(4, bob); + + // Verify final state + assertEq(harness.totalSupply(), 4); + assertEq(harness.balanceOf(bob), 2); + + // Verify owned tokens (order may have changed due to swap-and-pop) + assertEq(harness.tokenOfOwnerByIndex(alice, 0), 1); + assertEq(harness.tokenOfOwnerByIndex(alice, 1), 3); + assertEq(harness.tokenOfOwnerByIndex(bob, 0), 2); + assertEq(harness.tokenOfOwnerByIndex(bob, 1), 5); + } + + // ============================================ + // Edge Cases + // ============================================ + + function test_Mint_MaxUint256TokenId() public { + uint256 maxTokenId = type(uint256).max; + harness.mint(alice, maxTokenId); + + assertEq(harness.ownerOf(maxTokenId), alice); + assertEq(harness.balanceOf(alice), 1); + } + + function test_Burn_OnlyTokenOwned() public { + harness.mint(alice, 1); + harness.burn(1, alice); + + assertEq(harness.balanceOf(alice), 0); + assertEq(harness.totalSupply(), 0); + } + + function test_Transfer_LastTokenInCollection() public { + harness.mint(alice, 1); + harness.mint(alice, 2); + harness.mint(alice, 3); + + // Transfer the last token + harness.transferFrom(alice, bob, 3, alice); + + assertEq(harness.balanceOf(alice), 2); + assertEq(harness.balanceOf(bob), 1); + assertEq(harness.tokenOfOwnerByIndex(alice, 0), 1); + assertEq(harness.tokenOfOwnerByIndex(alice, 1), 2); + assertEq(harness.tokenOfOwnerByIndex(bob, 0), 3); + } +} + diff --git a/test/token/ERC721/ERC721Enumerable/harnesses/ERC721EnumerableFacetHarness.sol b/test/token/ERC721/ERC721Enumerable/harnesses/ERC721EnumerableFacetHarness.sol new file mode 100644 index 00000000..8483a72a --- /dev/null +++ b/test/token/ERC721/ERC721Enumerable/harnesses/ERC721EnumerableFacetHarness.sol @@ -0,0 +1,73 @@ +// 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 helper functions for testing +/// @dev Extends ERC721EnumerableFacet with mint and burn capabilities for testing +contract ERC721EnumerableFacetHarness is ERC721EnumerableFacet { + /// @notice Initialize the ERC721 enumerable token storage + /// @dev Only used for testing + 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 Mint a token for testing + 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.ownedTokensIndexOf[_tokenId] = s.ownedTokensOf[_to].length; + s.ownedTokensOf[_to].push(_tokenId); + s.allTokensIndexOf[_tokenId] = s.allTokens.length; + s.allTokens.push(_tokenId); + s.ownerOf[_tokenId] = _to; + emit Transfer(address(0), _to, _tokenId); + } + + /// @notice Burn a token for testing + function burn(uint256 _tokenId) external { + ERC721EnumerableStorage storage s = getStorage(); + address owner = s.ownerOf[_tokenId]; + if (owner == address(0)) { + revert ERC721NonexistentToken(_tokenId); + } + if (msg.sender != owner) { + if (!s.isApprovedForAll[owner][msg.sender] && msg.sender != s.approved[_tokenId]) { + revert ERC721InsufficientApproval(msg.sender, _tokenId); + } + } + + delete s.ownerOf[_tokenId]; + delete s.approved[_tokenId]; + + uint256 tokenIndex = s.ownedTokensIndexOf[_tokenId]; + uint256 lastTokenIndex = s.ownedTokensOf[owner].length - 1; + if (tokenIndex != lastTokenIndex) { + uint256 lastTokenId = s.ownedTokensOf[owner][lastTokenIndex]; + s.ownedTokensOf[owner][tokenIndex] = lastTokenId; + s.ownedTokensIndexOf[lastTokenId] = tokenIndex; + } + s.ownedTokensOf[owner].pop(); + + tokenIndex = s.allTokensIndexOf[_tokenId]; + lastTokenIndex = s.allTokens.length - 1; + if (tokenIndex != lastTokenIndex) { + uint256 lastTokenId = s.allTokens[lastTokenIndex]; + s.allTokens[tokenIndex] = lastTokenId; + s.allTokensIndexOf[lastTokenId] = tokenIndex; + } + s.allTokens.pop(); + emit Transfer(owner, address(0), _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..2370da24 --- /dev/null +++ b/test/token/ERC721/ERC721Enumerable/harnesses/LibERC721EnumerableHarness.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {LibERC721} from "../../../../../src/token/ERC721/ERC721Enumerable/LibERC721Enumerable.sol"; + +/// @title LibERC721EnumerableHarness +/// @notice Test harness that exposes LibERC721 Enumerable's internal functions as external +/// @dev Required for testing since LibERC721 only has internal 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 { + LibERC721.ERC721EnumerableStorage storage s = LibERC721.getStorage(); + s.name = _name; + s.symbol = _symbol; + s.baseURI = _baseURI; + } + + /// @notice Exposes LibERC721.mint as an external function + function mint(address _to, uint256 _tokenId) external { + LibERC721.mint(_to, _tokenId); + } + + /// @notice Exposes LibERC721.burn as an external function + function burn(uint256 _tokenId, address _sender) external { + LibERC721.burn(_tokenId, _sender); + } + + /// @notice Exposes LibERC721.transferFrom as an external function + function transferFrom(address _from, address _to, uint256 _tokenId, address _sender) external { + LibERC721.transferFrom(_from, _to, _tokenId, _sender); + } + + /// @notice Get storage values for testing + function name() external view returns (string memory) { + return LibERC721.getStorage().name; + } + + function symbol() external view returns (string memory) { + return LibERC721.getStorage().symbol; + } + + function baseURI() external view returns (string memory) { + return LibERC721.getStorage().baseURI; + } + + function ownerOf(uint256 _tokenId) external view returns (address) { + return LibERC721.getStorage().ownerOf[_tokenId]; + } + + function balanceOf(address _owner) external view returns (uint256) { + return LibERC721.getStorage().ownedTokensOf[_owner].length; + } + + function totalSupply() external view returns (uint256) { + return LibERC721.getStorage().allTokens.length; + } + + function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256) { + return LibERC721.getStorage().ownedTokensOf[_owner][_index]; + } + + function tokenByIndex(uint256 _index) external view returns (uint256) { + return LibERC721.getStorage().allTokens[_index]; + } + + function ownedTokensIndexOf(uint256 _tokenId) external view returns (uint256) { + return LibERC721.getStorage().ownedTokensIndexOf[_tokenId]; + } + + function allTokensIndexOf(uint256 _tokenId) external view returns (uint256) { + return LibERC721.getStorage().allTokensIndexOf[_tokenId]; + } + + function approved(uint256 _tokenId) external view returns (address) { + return LibERC721.getStorage().approved[_tokenId]; + } + + function isApprovedForAll(address _owner, address _operator) external view returns (bool) { + return LibERC721.getStorage().isApprovedForAll[_owner][_operator]; + } +} +