diff --git a/test/token/ERC721/ERC721/ERC721BurnFacet.t.sol b/test/token/ERC721/ERC721/ERC721BurnFacet.t.sol new file mode 100644 index 00000000..92fceb83 --- /dev/null +++ b/test/token/ERC721/ERC721/ERC721BurnFacet.t.sol @@ -0,0 +1,56 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {ERC721BurnFacet} from "../../../../src/token/ERC721/ERC721/ERC721BurnFacet.sol"; +import {ERC721BurnFacetHarness} from "./harnesses/ERC721BurnFacetHarness.sol"; + +contract ERC721BurnFacetTest is Test { + ERC721BurnFacetHarness public harness; + + address public alice; + address public bob; + address public charlie; + + function setUp() public { + alice = makeAddr("alice"); + bob = makeAddr("bob"); + charlie = makeAddr("charlie"); + + harness = new ERC721BurnFacetHarness(); + } + + // ============================================ + // Burn Tests + // ============================================ + + function test_Burn() public { + uint256 tokenId = 7; + + harness.mint(alice, tokenId); + assertEq(harness.ownerOf(tokenId), alice); + + vm.prank(alice); + harness.burn(tokenId); + assertEq(harness.ownerOf(tokenId), address(0)); + } + + 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)); + } + + function test_BurnRevertWhenNonExistentToken() public { + uint256 tokenId = 888; + + vm.expectRevert(abi.encodeWithSelector(ERC721BurnFacet.ERC721NonexistentToken.selector, tokenId)); + harness.burn(tokenId); + } +} diff --git a/test/token/ERC721/ERC721/ERC721Facet.t.sol b/test/token/ERC721/ERC721/ERC721Facet.t.sol new file mode 100644 index 00000000..22b78ef6 --- /dev/null +++ b/test/token/ERC721/ERC721/ERC721Facet.t.sol @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {ERC721FacetHarness} from "./harnesses/ERC721FacetHarness.sol"; +import {ERC721Facet} from "../../../../../src/token/ERC721/ERC721/ERC721Facet.sol"; + +contract ERC721FacetTest is Test { + ERC721FacetHarness 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/"; + + function setUp() public { + alice = makeAddr("alice"); + bob = makeAddr("bob"); + charlie = makeAddr("charlie"); + + harness = new ERC721FacetHarness(); + 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); + } + + // ============================================ + // TokenURI Tests + // ============================================ + + function test_tokenURI() public { + uint256 tokenId = 1; + string memory expectedURI = string(abi.encodePacked(BASE_URI, "1")); + + harness.mint(alice, tokenId); + + string memory tokenURI = ERC721Facet(address(harness)).tokenURI(tokenId); + assertEq(tokenURI, expectedURI); + } + + function test_tokenOwner() public { + uint256 tokenId = 45; + + harness.mint(alice, tokenId); + + assertEq(harness.ownerOf(tokenId), alice); + } + + // ============================================ + // Approve Tests + // ============================================ + + function test_Approve() public { + uint256 tokenId = 4; + + harness.mint(alice, tokenId); + + vm.prank(alice); + ERC721Facet(address(harness)).approve(bob, tokenId); + + address approved = ERC721Facet(address(harness)).getApproved(tokenId); + assertEq(approved, bob); + } + + function test_ApproveSelfApproval() public { + uint256 tokenId = 6; + + harness.mint(bob, tokenId); + + vm.prank(bob); + ERC721Facet(address(harness)).approve(bob, tokenId); + + address approved = ERC721Facet(address(harness)).getApproved(tokenId); + assertEq(approved, bob); + } + + function test_ApproveClearsOnTransfer() public { + uint256 tokenId = 7; + + harness.mint(alice, tokenId); + + vm.prank(alice); + ERC721Facet(address(harness)).approve(bob, tokenId); + + vm.prank(alice); + ERC721Facet(address(harness)).transferFrom(alice, charlie, tokenId); + + address approved = ERC721Facet(address(harness)).getApproved(tokenId); + assertEq(approved, address(0)); + } + + function test_ApproveFuzz(address owner, address operator, uint256 tokenId) public { + vm.assume(owner != address(0)); + vm.assume(operator != address(0)); + vm.assume(owner != operator); + vm.assume(tokenId < type(uint256).max); + + harness.mint(owner, tokenId); + + vm.prank(owner); + ERC721Facet(address(harness)).approve(operator, tokenId); + + address approved = ERC721Facet(address(harness)).getApproved(tokenId); + assertEq(approved, operator); + } + + function test_getApproved() public { + uint256 tokenId = 4; + + harness.mint(alice, tokenId); + + vm.prank(alice); + ERC721Facet(address(harness)).approve(bob, tokenId); + + address approved = ERC721Facet(address(harness)).getApproved(tokenId); + assertEq(approved, bob); + + assertEq(harness.getApproved(tokenId), bob); + } + + // =========================================== + // SetApprovalForAll Tests + // =========================================== + + function test_SetApprovalForAll() public { + vm.prank(alice); + ERC721Facet(address(harness)).setApprovalForAll(bob, true); + + bool isApproved = ERC721Facet(address(harness)).isApprovedForAll(alice, bob); + assertTrue(isApproved); + } + + function test_SetApprovalForAllFuzz(address owner, address operator) public { + vm.assume(owner != address(0)); + vm.assume(operator != address(0)); + vm.assume(owner != operator); + + vm.prank(owner); + ERC721Facet(address(harness)).setApprovalForAll(operator, true); + + bool isApproved = ERC721Facet(address(harness)).isApprovedForAll(owner, operator); + assertTrue(isApproved); + } + + // ============================================ + // transferFrom tests + // ============================================ + + function test_transferFrom() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + assertEq(harness.ownerOf(tokenId), alice); + + vm.prank(alice); + harness.transferFrom(alice, bob, tokenId); + assertEq(harness.ownerOf(tokenId), bob); + } + + function test_transferFromToSelf() public { + uint256 tokenId = 2; + + harness.mint(charlie, tokenId); + assertEq(harness.ownerOf(tokenId), charlie); + + vm.prank(charlie); + harness.transferFrom(charlie, charlie, tokenId); + assertEq(harness.ownerOf(tokenId), charlie); + } + + 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); + assertEq(harness.ownerOf(tokenId), from); + + vm.prank(from); + harness.transferFrom(from, to, tokenId); + assertEq(harness.ownerOf(tokenId), to); + } + + function test_transferFromRevertWhenTransferFromNonExistentToken() public { + uint256 tokenId = 999; + + vm.expectRevert(abi.encodeWithSelector(ERC721Facet.ERC721NonexistentToken.selector, tokenId)); + harness.transferFrom(alice, bob, tokenId); + } + + // =========================================== + // safeTransferFrom Tests + // =========================================== + + function test_safeTransferFrom() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + assertEq(harness.ownerOf(tokenId), alice); + + vm.prank(alice); + harness.safeTransferFrom(alice, bob, tokenId); + assertEq(harness.ownerOf(tokenId), bob); + } + + function test_safeTransferFromToSelf() public { + uint256 tokenId = 2; + + harness.mint(charlie, tokenId); + assertEq(harness.ownerOf(tokenId), charlie); + + vm.prank(charlie); + harness.safeTransferFrom(charlie, charlie, tokenId); + assertEq(harness.ownerOf(tokenId), charlie); + } + + // ==================================== + // balanceOf Tests + // ==================================== + + function test_BalanceOf() public { + uint256 tokenId1 = 32; + uint256 tokenId2 = 45; + + harness.mint(alice, tokenId1); + harness.mint(alice, tokenId2); + + assertEq(harness.balanceOf(alice), 2); + } +} diff --git a/test/token/ERC721/ERC721/LibERC721.t.sol b/test/token/ERC721/ERC721/LibERC721.t.sol new file mode 100644 index 00000000..02de549d --- /dev/null +++ b/test/token/ERC721/ERC721/LibERC721.t.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {LibERC721} from "../../../../../src/token/ERC721/ERC721/LibERC721.sol"; +import {LibERC721Harness} from "./harnesses/LibERC721Harness.sol"; + +contract ERC721Test is Test { + LibERC721Harness 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/"; + + function setUp() public { + alice = makeAddr("alice"); + bob = makeAddr("bob"); + charlie = makeAddr("charlie"); + + harness = new LibERC721Harness(); + 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); + } + + // ============================================ + // TransferFrom Tests + // ============================================ + + function test_TransferFrom() public { + uint256 tokenId = 1; + + harness.mint(alice, tokenId); + assertEq(harness.ownerOf(tokenId), alice); + + vm.prank(alice); + harness.transferFrom(alice, bob, tokenId); + assertEq(harness.ownerOf(tokenId), bob); + } + + function test_TransferToSelf() public { + uint256 tokenId = 2; + + harness.mint(charlie, tokenId); + assertEq(harness.ownerOf(tokenId), charlie); + + vm.prank(charlie); + harness.transferFrom(charlie, charlie, tokenId); + assertEq(harness.ownerOf(tokenId), charlie); + } + + function test_TransferFuzz(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); + assertEq(harness.ownerOf(tokenId), from); + + vm.prank(from); + harness.transferFrom(from, to, tokenId); + assertEq(harness.ownerOf(tokenId), to); + } + + function test_TransferRevertWhenTransferFromNonExistentToken() public { + uint256 tokenId = 999; + + vm.expectRevert(abi.encodeWithSelector(LibERC721.ERC721NonexistentToken.selector, tokenId)); + harness.transferFrom(alice, bob, tokenId); + } + + function test_TransferRevertWhenSenderIsNotOwnerOrApproved() public { + uint256 tokenId = 4; + + harness.mint(alice, tokenId); + assertEq(harness.ownerOf(tokenId), alice); + + vm.prank(bob); + vm.expectRevert(abi.encodeWithSelector(LibERC721.ERC721InsufficientApproval.selector, bob, tokenId)); + harness.transferFrom(alice, charlie, tokenId); + } + + // ============================================ + // Mint Tests + // ============================================ + + function test_Mint() public { + uint256 tokenId = 5; + + harness.mint(bob, tokenId); + assertEq(harness.ownerOf(tokenId), bob); + } + + function test_MintMultiple() public { + for (uint256 tokenId = 1; tokenId <= 10; tokenId++) { + harness.mint(charlie, tokenId); + assertEq(harness.ownerOf(tokenId), charlie); + } + } + + 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); + } + + function test_MintRevertWhenInvalidReceiver() public { + uint256 tokenId = 6; + + vm.expectRevert(abi.encodeWithSelector(LibERC721.ERC721InvalidReceiver.selector, address(0))); + harness.mint(address(0), tokenId); + } + + // ============================================ + // Burn Tests + // ============================================ + + function test_Burn() public { + uint256 tokenId = 7; + + harness.mint(alice, tokenId); + assertEq(harness.ownerOf(tokenId), alice); + + harness.burn(tokenId); + assertEq(harness.ownerOf(tokenId), address(0)); + } + + 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); + + harness.burn(tokenId); + assertEq(harness.ownerOf(tokenId), address(0)); + } + + function test_BurnRevertWhenNonExistentToken() public { + uint256 tokenId = 888; + + vm.expectRevert(abi.encodeWithSelector(LibERC721.ERC721NonexistentToken.selector, tokenId)); + harness.burn(tokenId); + } +} diff --git a/test/token/ERC721/ERC721/harnesses/ERC721BurnFacetHarness.sol b/test/token/ERC721/ERC721/harnesses/ERC721BurnFacetHarness.sol new file mode 100644 index 00000000..e853bb86 --- /dev/null +++ b/test/token/ERC721/ERC721/harnesses/ERC721BurnFacetHarness.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {ERC721BurnFacet} from "../../../../../src/token/ERC721/ERC721/ERC721BurnFacet.sol"; + +contract ERC721BurnFacetHarness is ERC721BurnFacet { + /// @notice Initialize the ERC721 token storage + /// @dev Only used for testing - production diamonds should initialize in constructor + function initialize() external { + ERC721Storage storage s = getStorage(); + } + + /// @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) public { + ERC721Storage storage s = getStorage(); + require(_to != address(0), "ERC721InvalidReceiver"); + require(s.ownerOf[_tokenId] == address(0), "ERC721InvalidSender"); + s.ownerOf[_tokenId] = _to; + unchecked { + s.balanceOf[_to]++; + } + emit Transfer(address(0), _to, _tokenId); + } + + /// @notice Returns the owner of a given token ID (raw, does not revert for non-existent tokens). + /// @param _tokenId The token ID to query. + /// @return The address of the token owner (or address(0) if not minted). + function ownerOf(uint256 _tokenId) public view returns (address) { + return getStorage().ownerOf[_tokenId]; + } +} diff --git a/test/token/ERC721/ERC721/harnesses/ERC721FacetHarness.sol b/test/token/ERC721/ERC721/harnesses/ERC721FacetHarness.sol new file mode 100644 index 00000000..a2723333 --- /dev/null +++ b/test/token/ERC721/ERC721/harnesses/ERC721FacetHarness.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {ERC721Facet} from "../../../../../src/token/ERC721/ERC721/ERC721Facet.sol"; + +contract ERC721FacetHarness is ERC721Facet { + /// @notice Initialize the ERC721 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 { + ERC721Storage storage s = getStorage(); + s.name = _name; + s.symbol = _symbol; + s.baseURI = _baseURI; + } + + function baseURI() external view returns (string memory) { + return ERC721Facet.getStorage().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) public { + ERC721Storage storage s = getStorage(); + require(_to != address(0), "ERC721InvalidReceiver"); + require(s.ownerOf[_tokenId] == address(0), "ERC721InvalidSender"); + s.ownerOf[_tokenId] = _to; + unchecked { + s.balanceOf[_to]++; + } + emit Transfer(address(0), _to, _tokenId); + } +} diff --git a/test/token/ERC721/ERC721/harnesses/LibERC721Harness.sol b/test/token/ERC721/ERC721/harnesses/LibERC721Harness.sol new file mode 100644 index 00000000..5ea7eb1a --- /dev/null +++ b/test/token/ERC721/ERC721/harnesses/LibERC721Harness.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {LibERC721} from "../../../../../src/token/ERC721/ERC721/LibERC721.sol"; + +contract LibERC721Harness { + /// @notice Initialize the ERC721 token storage + /// @dev Only used for testing + function initialize(string memory _name, string memory _symbol, string memory _baseURI) external { + LibERC721.ERC721Storage 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) external { + LibERC721.burn(_tokenId); + } + + /// @notice Exposes LibERC721.transferFrom as an external function + function transferFrom(address _from, address _to, uint256 _tokenId) external { + LibERC721.transferFrom(_from, _to, _tokenId); + } + + /// @notice Expose owner lookup for a given token id + function ownerOf(uint256 _tokenId) external view returns (address) { + return LibERC721.getStorage().ownerOf[_tokenId]; + } + + /// @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; + } +}