From 06bd97da914bff27443a9e7d02464d52471c2ecb Mon Sep 17 00:00:00 2001 From: Abidoyesimze Date: Fri, 31 Oct 2025 08:07:08 +0100 Subject: [PATCH] fix: set ownerOf in LibERC721Enumerable mint function The mint function in LibERC721Enumerable was missing the critical s.ownerOf[_tokenId] = _to assignment, causing newly minted tokens to have no recorded owner. This resulted in ownerOf() queries failing and transfers being impossible. This fix adds the missing line to properly set token ownership during minting, matching the implementation in the standard LibERC721 library. Includes comprehensive test coverage: - 32 tests for LibERC721Enumerable (mint, burn, transfer, enumeration) - 42 tests for ERC721EnumerableFacet (all public functions) --- .../ERC721Enumerable/LibERC721Enumerable.sol | 1 + .../ERC721EnumerableFacet.t.sol | 471 ++++++++++++++++++ .../LibERC721Enumerable.t.sol | 406 +++++++++++++++ .../ERC721EnumerableFacetHarness.sol | 73 +++ .../harnesses/LibERC721EnumerableHarness.sol | 83 +++ 5 files changed, 1034 insertions(+) create mode 100644 test/token/ERC721/ERC721Enumerable/ERC721EnumerableFacet.t.sol create mode 100644 test/token/ERC721/ERC721Enumerable/LibERC721Enumerable.t.sol create mode 100644 test/token/ERC721/ERC721Enumerable/harnesses/ERC721EnumerableFacetHarness.sol create mode 100644 test/token/ERC721/ERC721Enumerable/harnesses/LibERC721EnumerableHarness.sol 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]; + } +} +