diff --git a/test/trees/ERC721.tree b/test/trees/ERC721.tree new file mode 100644 index 00000000..26633acd --- /dev/null +++ b/test/trees/ERC721.tree @@ -0,0 +1,140 @@ +BalanceOf +├── when the owner is the zero address +│ └── it should revert +└── when the owner is not the zero address + └── it should return the number of tokens owned by the owner + +OwnerOf +├── when the token does not exist +│ └── it should revert +└── when the token exists + └── it should return the owner of the token + +Approve +├── when the token does not exist +│ └── it should revert +└── when the token exists + ├── when the caller is not the owner and not an approved operator + │ └── it should revert + └── when the caller is the owner or an approved operator + ├── it should set the approved address for the token + └── it should emit an {Approval} event + +SetApprovalForAll +├── when the operator is the zero address +│ └── it should revert +└── when the operator is not the zero address + ├── it should set the operator approval status for the caller + └── it should emit an {ApprovalForAll} event + +GetApproved +├── when the token does not exist +│ └── it should revert +└── when the token exists + └── it should return the approved address for the token + +IsApprovedForAll +└── it should return the operator approval status + +TransferFrom +├── when the token does not exist +│ └── it should revert +└── when the token exists + ├── when the from address is not the owner + │ └── it should revert + └── when the from address is the owner + ├── when the to address is the zero address + │ └── it should revert + └── when the to address is not the zero address + ├── when the caller is not authorized + │ └── it should revert + └── when the caller is authorized + ├── it should transfer ownership of the token to the recipient + ├── it should decrement the sender's balance + ├── it should increment the receiver's balance + ├── it should clear the approval for the token + └── it should emit a {Transfer} event + +SafeTransferFrom (without data) +├── when the token does not exist +│ └── it should revert +└── when the token exists + ├── when the from address is not the owner + │ └── it should revert + └── when the from address is the owner + ├── when the to address is the zero address + │ └── it should revert + └── when the to address is not the zero address + ├── when the caller is not authorized + │ └── it should revert + └── when the caller is authorized + ├── when the receiver is a contract that does not implement onERC721Received + │ └── it should revert + ├── when the receiver is a contract that returns an incorrect value + │ └── it should revert + └── when the receiver is an EOA or returns the correct selector + ├── it should transfer ownership of the token to the recipient + ├── it should decrement the sender's balance + ├── it should increment the receiver's balance + ├── it should clear the approval for the token + └── it should emit a {Transfer} event + +SafeTransferFrom (with data) +├── when the token does not exist +│ └── it should revert +└── when the token exists + ├── when the from address is not the owner + │ └── it should revert + └── when the from address is the owner + ├── when the to address is the zero address + │ └── it should revert + └── when the to address is not the zero address + ├── when the caller is not authorized + │ └── it should revert + └── when the caller is authorized + ├── when the receiver is a contract that does not implement onERC721Received + │ └── it should revert + ├── when the receiver is a contract that returns an incorrect value + │ └── it should revert + └── when the receiver is an EOA or returns the correct selector + ├── it should transfer ownership of the token to the recipient + ├── it should decrement the sender's balance + ├── it should increment the receiver's balance + ├── it should clear the approval for the token + ├── it should pass the data to onERC721Received + └── it should emit a {Transfer} event + +Mint +├── when the recipient is the zero address +│ └── it should revert +└── when the recipient is not the zero address + ├── when the token already exists + │ └── it should revert + └── when the token does not exist + ├── it should assign the token to the recipient + ├── it should increment the recipient's balance + └── it should emit a {Transfer} event from the zero address + +Burn +├── when the token does not exist +│ └── it should revert +└── when the token exists + ├── when the caller is not authorized + │ └── it should revert + └── when the caller is authorized + ├── it should remove the token (set owner to zero address) + ├── it should decrement the owner's balance + ├── it should clear the approval for the token + └── it should emit a {Transfer} event to the zero address + +Name +└── it should return the token collection name + +Symbol +└── it should return the token collection symbol + +TokenURI +├── when the token does not exist +│ └── it should revert +└── when the token exists + └── it should return the token URI diff --git a/test/unit/token/ERC721/Approve/facet/ERC721ApproveFacetBase.t.sol b/test/unit/token/ERC721/Approve/facet/ERC721ApproveFacetBase.t.sol new file mode 100644 index 00000000..f5c3cc4e --- /dev/null +++ b/test/unit/token/ERC721/Approve/facet/ERC721ApproveFacetBase.t.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30 <0.9.0; + +/* Compose + * https://compose.diamonds + */ + +import {Base_Test} from "test/Base.t.sol"; +import {ERC721ApproveFacet} from "src/token/ERC721/Approve/ERC721ApproveFacet.sol"; + +contract ERC721ApproveFacet_Base_Test is Base_Test { + ERC721ApproveFacet internal facet; + + function setUp() public virtual override { + Base_Test.setUp(); + facet = new ERC721ApproveFacet(); + vm.label(address(facet), "ERC721ApproveFacet"); + } +} diff --git a/test/unit/token/ERC721/Approve/facet/fuzz/approve.t.sol b/test/unit/token/ERC721/Approve/facet/fuzz/approve.t.sol new file mode 100644 index 00000000..184bca18 --- /dev/null +++ b/test/unit/token/ERC721/Approve/facet/fuzz/approve.t.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {ERC721ApproveFacet_Base_Test} from "../ERC721ApproveFacetBase.t.sol"; +import {ERC721StorageUtils} from "test/utils/storage/ERC721StorageUtils.sol"; + +import {ERC721ApproveFacet} from "src/token/ERC721/Approve/ERC721ApproveFacet.sol"; + +/** + * @dev BTT spec: test/trees/ERC721.tree + */ +contract Approve_ERC721ApproveFacet_Fuzz_Unit_Test is ERC721ApproveFacet_Base_Test { + using ERC721StorageUtils for address; + + function testFuzz_ShouldRevert_TokenDoesNotExist(address to, uint256 tokenId) external { + vm.expectRevert(abi.encodeWithSelector(ERC721ApproveFacet.ERC721NonexistentToken.selector, tokenId)); + facet.approve(to, tokenId); + } + + function testFuzz_ShouldRevert_CallerNotOwnerAndNotOperator(address owner, address to, uint256 tokenId) + external + whenTokenExists + { + vm.assume(owner != ADDRESS_ZERO); + vm.assume(owner != users.alice); + + address(facet).mint(owner, tokenId); + + vm.expectRevert(abi.encodeWithSelector(ERC721ApproveFacet.ERC721InvalidApprover.selector, users.alice)); + facet.approve(to, tokenId); + } + + function testFuzz_Approve_CallerIsOwner(address to, uint256 tokenId) external whenTokenExists { + address(facet).mint(users.alice, tokenId); + + vm.expectEmit(address(facet)); + emit ERC721ApproveFacet.Approval(users.alice, to, tokenId); + facet.approve(to, tokenId); + + assertEq(address(facet).getApproved(tokenId), to, "getApproved(tokenId)"); + } + + function testFuzz_Approve_CallerIsApprovedOperator(address owner, address to, uint256 tokenId) + external + whenTokenExists + { + vm.assume(owner != ADDRESS_ZERO); + vm.assume(owner != users.alice); + + address(facet).mint(owner, tokenId); + address(facet).setApprovalForAll(owner, users.alice, true); + + vm.expectEmit(address(facet)); + emit ERC721ApproveFacet.Approval(owner, to, tokenId); + facet.approve(to, tokenId); + + assertEq(address(facet).getApproved(tokenId), to, "getApproved(tokenId)"); + } +} diff --git a/test/unit/token/ERC721/Approve/facet/fuzz/setApprovalForAll.t.sol b/test/unit/token/ERC721/Approve/facet/fuzz/setApprovalForAll.t.sol new file mode 100644 index 00000000..e841d074 --- /dev/null +++ b/test/unit/token/ERC721/Approve/facet/fuzz/setApprovalForAll.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {ERC721ApproveFacet_Base_Test} from "../ERC721ApproveFacetBase.t.sol"; +import {ERC721StorageUtils} from "test/utils/storage/ERC721StorageUtils.sol"; + +import {ERC721ApproveFacet} from "src/token/ERC721/Approve/ERC721ApproveFacet.sol"; + +/** + * @dev BTT spec: test/trees/ERC721.tree + */ +contract SetApprovalForAll_ERC721ApproveFacet_Fuzz_Unit_Test is ERC721ApproveFacet_Base_Test { + using ERC721StorageUtils for address; + + function testFuzz_ShouldRevert_OperatorIsZeroAddress(bool approved) external { + vm.expectRevert(abi.encodeWithSelector(ERC721ApproveFacet.ERC721InvalidOperator.selector, ADDRESS_ZERO)); + facet.setApprovalForAll(ADDRESS_ZERO, approved); + } + + function testFuzz_SetApprovalForAll_ApproveTrue(address operator) external whenOperatorNotZeroAddress { + vm.assume(operator != ADDRESS_ZERO); + + vm.expectEmit(address(facet)); + emit ERC721ApproveFacet.ApprovalForAll(users.alice, operator, true); + facet.setApprovalForAll(operator, true); + + assertEq(address(facet).isApprovedForAll(users.alice, operator), true, "isApprovedForAll"); + } + + function testFuzz_SetApprovalForAll_ApproveFalse(address operator) external whenOperatorNotZeroAddress { + vm.assume(operator != ADDRESS_ZERO); + + // First set to true + address(facet).setApprovalForAll(users.alice, operator, true); + + vm.expectEmit(address(facet)); + emit ERC721ApproveFacet.ApprovalForAll(users.alice, operator, false); + facet.setApprovalForAll(operator, false); + + assertEq(address(facet).isApprovedForAll(users.alice, operator), false, "isApprovedForAll"); + } +} diff --git a/test/unit/token/ERC721/Burn/facet/fuzz/burnERC721.t.sol b/test/unit/token/ERC721/Burn/facet/fuzz/burnERC721.t.sol new file mode 100644 index 00000000..d8987a85 --- /dev/null +++ b/test/unit/token/ERC721/Burn/facet/fuzz/burnERC721.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {Base_Test} from "test/Base.t.sol"; +import {ERC721StorageUtils} from "test/utils/storage/ERC721StorageUtils.sol"; + +import {ERC721BurnFacet} from "src/token/ERC721/Burn/ERC721BurnFacet.sol"; + +/** + * @dev BTT spec: test/trees/ERC721.tree + */ +contract BurnERC721_ERC721BurnFacet_Fuzz_Unit_Test is Base_Test { + using ERC721StorageUtils for address; + + ERC721BurnFacet internal facet; + + function setUp() public virtual override { + Base_Test.setUp(); + facet = new ERC721BurnFacet(); + vm.label(address(facet), "ERC721BurnFacet"); + } + + function testFuzz_ShouldRevert_TokenDoesNotExist(uint256 tokenId) external { + vm.expectRevert(abi.encodeWithSelector(ERC721BurnFacet.ERC721NonexistentToken.selector, tokenId)); + facet.burnERC721(tokenId); + } + + function testFuzz_ShouldRevert_CallerNotAuthorized(address owner, uint256 tokenId) external whenTokenExists { + vm.assume(owner != ADDRESS_ZERO); + vm.assume(owner != users.alice); + + address(facet).mint(owner, tokenId); + + vm.expectRevert( + abi.encodeWithSelector(ERC721BurnFacet.ERC721InsufficientApproval.selector, users.alice, tokenId) + ); + facet.burnERC721(tokenId); + } + + function testFuzz_Burn_CallerIsOwner(uint256 tokenId, address approved) + external + whenTokenExists + whenCallerIsAuthorized + { + address(facet).mint(users.alice, tokenId); + address(facet).setApproved(tokenId, approved); + + uint256 balanceBefore = address(facet).balanceOf(users.alice); + + vm.expectEmit(address(facet)); + emit ERC721BurnFacet.Transfer(users.alice, ADDRESS_ZERO, tokenId); + facet.burnERC721(tokenId); + + assertEq(address(facet).ownerOf(tokenId), ADDRESS_ZERO, "ownerOf(tokenId)"); + assertEq(address(facet).balanceOf(users.alice), balanceBefore - 1, "balanceOf(owner)"); + assertEq(address(facet).getApproved(tokenId), ADDRESS_ZERO, "getApproved(tokenId)"); + } + + function testFuzz_Burn_CallerIsApprovedOperator(address owner, uint256 tokenId, address approved) + external + whenTokenExists + whenCallerIsAuthorized + { + vm.assume(owner != ADDRESS_ZERO); + vm.assume(owner != users.alice); + + address(facet).mint(owner, tokenId); + address(facet).setApprovalForAll(owner, users.alice, true); + address(facet).setApproved(tokenId, approved); + + uint256 balanceBefore = address(facet).balanceOf(owner); + + vm.expectEmit(address(facet)); + emit ERC721BurnFacet.Transfer(owner, ADDRESS_ZERO, tokenId); + facet.burnERC721(tokenId); + + assertEq(address(facet).ownerOf(tokenId), ADDRESS_ZERO, "ownerOf(tokenId)"); + assertEq(address(facet).balanceOf(owner), balanceBefore - 1, "balanceOf(owner)"); + assertEq(address(facet).getApproved(tokenId), ADDRESS_ZERO, "getApproved(tokenId)"); + } + + function testFuzz_Burn_CallerIsTokenApproved(address owner, uint256 tokenId) + external + whenTokenExists + whenCallerIsAuthorized + { + vm.assume(owner != ADDRESS_ZERO); + vm.assume(owner != users.alice); + + address(facet).mint(owner, tokenId); + address(facet).setApproved(tokenId, users.alice); + + uint256 balanceBefore = address(facet).balanceOf(owner); + + vm.expectEmit(address(facet)); + emit ERC721BurnFacet.Transfer(owner, ADDRESS_ZERO, tokenId); + facet.burnERC721(tokenId); + + assertEq(address(facet).ownerOf(tokenId), ADDRESS_ZERO, "ownerOf(tokenId)"); + assertEq(address(facet).balanceOf(owner), balanceBefore - 1, "balanceOf(owner)"); + assertEq(address(facet).getApproved(tokenId), ADDRESS_ZERO, "getApproved(tokenId)"); + } +} diff --git a/test/unit/token/ERC721/Data/facet/ERC721DataFacetBase.t.sol b/test/unit/token/ERC721/Data/facet/ERC721DataFacetBase.t.sol new file mode 100644 index 00000000..88e7153f --- /dev/null +++ b/test/unit/token/ERC721/Data/facet/ERC721DataFacetBase.t.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30 <0.9.0; + +/* Compose + * https://compose.diamonds + */ + +import {Base_Test} from "test/Base.t.sol"; +import {ERC721DataFacet} from "src/token/ERC721/Data/ERC721DataFacet.sol"; + +contract ERC721DataFacet_Base_Test is Base_Test { + ERC721DataFacet internal facet; + + function setUp() public virtual override { + Base_Test.setUp(); + facet = new ERC721DataFacet(); + vm.label(address(facet), "ERC721DataFacet"); + } +} diff --git a/test/unit/token/ERC721/Data/facet/fuzz/balanceOf.t.sol b/test/unit/token/ERC721/Data/facet/fuzz/balanceOf.t.sol new file mode 100644 index 00000000..fec96ed3 --- /dev/null +++ b/test/unit/token/ERC721/Data/facet/fuzz/balanceOf.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {ERC721DataFacet_Base_Test} from "../ERC721DataFacetBase.t.sol"; +import {ERC721StorageUtils} from "test/utils/storage/ERC721StorageUtils.sol"; + +import {ERC721DataFacet} from "src/token/ERC721/Data/ERC721DataFacet.sol"; + +/** + * @dev BTT spec: test/trees/ERC721.tree + */ +contract BalanceOf_ERC721DataFacet_Fuzz_Unit_Test is ERC721DataFacet_Base_Test { + using ERC721StorageUtils for address; + + function testFuzz_ShouldRevert_OwnerIsZeroAddress() external { + vm.expectRevert(abi.encodeWithSelector(ERC721DataFacet.ERC721InvalidOwner.selector, ADDRESS_ZERO)); + facet.balanceOf(ADDRESS_ZERO); + } + + function testFuzz_BalanceOf(address owner, uint256 balance) external whenOwnerNotZeroAddress { + vm.assume(owner != ADDRESS_ZERO); + balance = bound(balance, 0, MAX_UINT256); + + address(facet).setBalanceOf(owner, balance); + + assertEq(facet.balanceOf(owner), balance, "balanceOf(owner)"); + } +} diff --git a/test/unit/token/ERC721/Data/facet/fuzz/getApproved.t.sol b/test/unit/token/ERC721/Data/facet/fuzz/getApproved.t.sol new file mode 100644 index 00000000..6ca57bd7 --- /dev/null +++ b/test/unit/token/ERC721/Data/facet/fuzz/getApproved.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {ERC721DataFacet_Base_Test} from "../ERC721DataFacetBase.t.sol"; +import {ERC721StorageUtils} from "test/utils/storage/ERC721StorageUtils.sol"; + +import {ERC721DataFacet} from "src/token/ERC721/Data/ERC721DataFacet.sol"; + +/** + * @dev BTT spec: test/trees/ERC721.tree + */ +contract GetApproved_ERC721DataFacet_Fuzz_Unit_Test is ERC721DataFacet_Base_Test { + using ERC721StorageUtils for address; + + function testFuzz_ShouldRevert_TokenDoesNotExist(uint256 tokenId) external { + vm.expectRevert(abi.encodeWithSelector(ERC721DataFacet.ERC721NonexistentToken.selector, tokenId)); + facet.getApproved(tokenId); + } + + function testFuzz_GetApproved(address owner, address approved, uint256 tokenId) external whenTokenExists { + vm.assume(owner != ADDRESS_ZERO); + + address(facet).setOwnerOf(tokenId, owner); + address(facet).setApproved(tokenId, approved); + + assertEq(facet.getApproved(tokenId), approved, "getApproved(tokenId)"); + } +} diff --git a/test/unit/token/ERC721/Data/facet/fuzz/isApprovedForAll.t.sol b/test/unit/token/ERC721/Data/facet/fuzz/isApprovedForAll.t.sol new file mode 100644 index 00000000..09d233da --- /dev/null +++ b/test/unit/token/ERC721/Data/facet/fuzz/isApprovedForAll.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {ERC721DataFacet_Base_Test} from "../ERC721DataFacetBase.t.sol"; +import {ERC721StorageUtils} from "test/utils/storage/ERC721StorageUtils.sol"; + +import {ERC721DataFacet} from "src/token/ERC721/Data/ERC721DataFacet.sol"; + +/** + * @dev BTT spec: test/trees/ERC721.tree + */ +contract IsApprovedForAll_ERC721DataFacet_Fuzz_Unit_Test is ERC721DataFacet_Base_Test { + using ERC721StorageUtils for address; + + function testFuzz_IsApprovedForAll(address owner, address operator, bool approved) external { + address(facet).setApprovalForAll(owner, operator, approved); + + assertEq(facet.isApprovedForAll(owner, operator), approved, "isApprovedForAll(owner, operator)"); + } +} diff --git a/test/unit/token/ERC721/Data/facet/fuzz/ownerOf.t.sol b/test/unit/token/ERC721/Data/facet/fuzz/ownerOf.t.sol new file mode 100644 index 00000000..e603b8f5 --- /dev/null +++ b/test/unit/token/ERC721/Data/facet/fuzz/ownerOf.t.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {ERC721DataFacet_Base_Test} from "../ERC721DataFacetBase.t.sol"; +import {ERC721StorageUtils} from "test/utils/storage/ERC721StorageUtils.sol"; + +import {ERC721DataFacet} from "src/token/ERC721/Data/ERC721DataFacet.sol"; + +/** + * @dev BTT spec: test/trees/ERC721.tree + */ +contract OwnerOf_ERC721DataFacet_Fuzz_Unit_Test is ERC721DataFacet_Base_Test { + using ERC721StorageUtils for address; + + function testFuzz_ShouldRevert_TokenDoesNotExist(uint256 tokenId) external { + vm.expectRevert(abi.encodeWithSelector(ERC721DataFacet.ERC721NonexistentToken.selector, tokenId)); + facet.ownerOf(tokenId); + } + + function testFuzz_OwnerOf(address owner, uint256 tokenId) external whenTokenExists { + vm.assume(owner != ADDRESS_ZERO); + + address(facet).setOwnerOf(tokenId, owner); + + assertEq(facet.ownerOf(tokenId), owner, "ownerOf(tokenId)"); + } +} diff --git a/test/unit/token/ERC721/Transfer/facet/ERC721TransferFacetBase.t.sol b/test/unit/token/ERC721/Transfer/facet/ERC721TransferFacetBase.t.sol new file mode 100644 index 00000000..fed8e0d1 --- /dev/null +++ b/test/unit/token/ERC721/Transfer/facet/ERC721TransferFacetBase.t.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30 <0.9.0; + +/* Compose + * https://compose.diamonds + */ + +import {Base_Test} from "test/Base.t.sol"; +import {ERC721TransferFacet} from "src/token/ERC721/Transfer/ERC721TransferFacet.sol"; + +contract MockERC721Receiver {} + +contract ERC721TransferFacet_Base_Test is Base_Test { + ERC721TransferFacet internal facet; + + MockERC721Receiver internal receiver; + + function setUp() public virtual override { + Base_Test.setUp(); + facet = new ERC721TransferFacet(); + vm.label(address(facet), "ERC721TransferFacet"); + + receiver = new MockERC721Receiver(); + vm.label(address(receiver), "MockERC721Receiver"); + } +} diff --git a/test/unit/token/ERC721/Transfer/facet/fuzz/safeTransferFrom.t.sol b/test/unit/token/ERC721/Transfer/facet/fuzz/safeTransferFrom.t.sol new file mode 100644 index 00000000..e82e8e0d --- /dev/null +++ b/test/unit/token/ERC721/Transfer/facet/fuzz/safeTransferFrom.t.sol @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {ERC721TransferFacet_Base_Test} from "../ERC721TransferFacetBase.t.sol"; +import {ERC721StorageUtils} from "test/utils/storage/ERC721StorageUtils.sol"; + +import {IERC721Receiver, ERC721TransferFacet} from "src/token/ERC721/Transfer/ERC721TransferFacet.sol"; + +/** + * @dev BTT spec: test/trees/ERC721.tree + */ +contract SafeTransferFrom_ERC721TransferFacet_Fuzz_Unit_Test is ERC721TransferFacet_Base_Test { + using ERC721StorageUtils for address; + + /*////////////////////////////////////////////////////////////// + SAFE TRANSFER FROM (WITHOUT DATA) + //////////////////////////////////////////////////////////////*/ + + function testFuzz_SafeTransferFrom_ShouldRevert_TokenDoesNotExist(address from, address to, uint256 tokenId) + external + { + vm.assume(to != ADDRESS_ZERO); + + vm.expectRevert(abi.encodeWithSelector(ERC721TransferFacet.ERC721NonexistentToken.selector, tokenId)); + facet.safeTransferFrom(from, to, tokenId); + } + + function testFuzz_SafeTransferFrom_ShouldRevert_FromNotOwner(address from, address to, uint256 tokenId) + external + whenTokenExists + { + vm.assume(to != ADDRESS_ZERO); + vm.assume(from != ADDRESS_ZERO); + vm.assume(from != users.alice); + + address(facet).mint(users.alice, tokenId); + + vm.expectRevert( + abi.encodeWithSelector(ERC721TransferFacet.ERC721IncorrectOwner.selector, from, tokenId, users.alice) + ); + facet.safeTransferFrom(from, to, tokenId); + } + + function testFuzz_SafeTransferFrom_ShouldRevert_ToIsZeroAddress(uint256 tokenId) + external + whenTokenExists + whenFromIsOwner + { + address(facet).mint(users.alice, tokenId); + + vm.expectRevert(abi.encodeWithSelector(ERC721TransferFacet.ERC721InvalidReceiver.selector, ADDRESS_ZERO)); + facet.safeTransferFrom(users.alice, ADDRESS_ZERO, tokenId); + } + + function testFuzz_SafeTransferFrom_ShouldRevert_CallerNotAuthorized(address owner, address to, uint256 tokenId) + external + whenTokenExists + whenFromIsOwner + whenToNotZeroAddress + { + vm.assume(owner != ADDRESS_ZERO); + vm.assume(owner != users.alice); + vm.assume(to != ADDRESS_ZERO); + + address(facet).mint(owner, tokenId); + + vm.expectRevert( + abi.encodeWithSelector(ERC721TransferFacet.ERC721InsufficientApproval.selector, users.alice, tokenId) + ); + facet.safeTransferFrom(owner, to, tokenId); + } + + function testFuzz_SafeTransferFrom_ShouldRevert_ReceiverDoesNotImplement(uint256 tokenId) + external + whenTokenExists + whenFromIsOwner + whenToNotZeroAddress + whenCallerIsAuthorized + { + address(facet).mint(users.alice, tokenId); + + vm.expectRevert(abi.encodeWithSelector(ERC721TransferFacet.ERC721InvalidReceiver.selector, address(receiver))); + facet.safeTransferFrom(users.alice, address(receiver), tokenId); + } + + function testFuzz_SafeTransferFrom_ShouldRevert_ReceiverReturnsIncorrectValue(uint256 tokenId, bytes4 returnedSelector) + external + whenTokenExists + whenFromIsOwner + whenToNotZeroAddress + whenCallerIsAuthorized + { + vm.assume(returnedSelector != IERC721Receiver.onERC721Received.selector); + + address(facet).mint(users.alice, tokenId); + + vm.mockCall( + address(receiver), + abi.encodeWithSelector(IERC721Receiver.onERC721Received.selector), + abi.encode(returnedSelector) + ); + + vm.expectRevert(abi.encodeWithSelector(ERC721TransferFacet.ERC721InvalidReceiver.selector, address(receiver))); + facet.safeTransferFrom(users.alice, address(receiver), tokenId); + } + + function testFuzz_SafeTransferFrom_ToEOA(address to, uint256 tokenId) + external + whenTokenExists + whenFromIsOwner + whenToNotZeroAddress + whenCallerIsAuthorized + { + vm.assume(to != ADDRESS_ZERO); + vm.assume(to != users.alice); + vm.assume(to.code.length == 0); + + address(facet).mint(users.alice, tokenId); + + uint256 senderBalanceBefore = address(facet).balanceOf(users.alice); + uint256 receiverBalanceBefore = address(facet).balanceOf(to); + + vm.expectEmit(address(facet)); + emit ERC721TransferFacet.Transfer(users.alice, to, tokenId); + facet.safeTransferFrom(users.alice, to, tokenId); + + assertEq(address(facet).ownerOf(tokenId), to, "ownerOf(tokenId)"); + assertEq(address(facet).balanceOf(users.alice), senderBalanceBefore - 1, "balanceOf(from)"); + assertEq(address(facet).balanceOf(to), receiverBalanceBefore + 1, "balanceOf(to)"); + assertEq(address(facet).getApproved(tokenId), ADDRESS_ZERO, "getApproved(tokenId)"); + } + + function testFuzz_SafeTransferFrom_ToContractWithCorrectSelector(uint256 tokenId) + external + whenTokenExists + whenFromIsOwner + whenToNotZeroAddress + whenCallerIsAuthorized + { + address(facet).mint(users.alice, tokenId); + + uint256 senderBalanceBefore = address(facet).balanceOf(users.alice); + + vm.mockCall( + address(receiver), + abi.encodeWithSelector(IERC721Receiver.onERC721Received.selector), + abi.encode(IERC721Receiver.onERC721Received.selector) + ); + + vm.expectEmit(address(facet)); + emit ERC721TransferFacet.Transfer(users.alice, address(receiver), tokenId); + facet.safeTransferFrom(users.alice, address(receiver), tokenId); + + assertEq(address(facet).ownerOf(tokenId), address(receiver), "ownerOf(tokenId)"); + assertEq(address(facet).balanceOf(users.alice), senderBalanceBefore - 1, "balanceOf(from)"); + assertEq(address(facet).balanceOf(address(receiver)), 1, "balanceOf(to)"); + assertEq(address(facet).getApproved(tokenId), ADDRESS_ZERO, "getApproved(tokenId)"); + } + + /*////////////////////////////////////////////////////////////// + SAFE TRANSFER FROM (WITH DATA) + //////////////////////////////////////////////////////////////*/ + + function testFuzz_SafeTransferFromWithData_ShouldRevert_TokenDoesNotExist( + address from, + address to, + uint256 tokenId, + bytes calldata data + ) external { + vm.assume(to != ADDRESS_ZERO); + + vm.expectRevert(abi.encodeWithSelector(ERC721TransferFacet.ERC721NonexistentToken.selector, tokenId)); + facet.safeTransferFrom(from, to, tokenId, data); + } + + function testFuzz_SafeTransferFromWithData_ShouldRevert_FromNotOwner( + address from, + address to, + uint256 tokenId, + bytes calldata data + ) external whenTokenExists { + vm.assume(to != ADDRESS_ZERO); + vm.assume(from != ADDRESS_ZERO); + vm.assume(from != users.alice); + + address(facet).mint(users.alice, tokenId); + + vm.expectRevert( + abi.encodeWithSelector(ERC721TransferFacet.ERC721IncorrectOwner.selector, from, tokenId, users.alice) + ); + facet.safeTransferFrom(from, to, tokenId, data); + } + + function testFuzz_SafeTransferFromWithData_ShouldRevert_ToIsZeroAddress(uint256 tokenId, bytes calldata data) + external + whenTokenExists + whenFromIsOwner + { + address(facet).mint(users.alice, tokenId); + + vm.expectRevert(abi.encodeWithSelector(ERC721TransferFacet.ERC721InvalidReceiver.selector, ADDRESS_ZERO)); + facet.safeTransferFrom(users.alice, ADDRESS_ZERO, tokenId, data); + } + + function testFuzz_SafeTransferFromWithData_ShouldRevert_CallerNotAuthorized( + address owner, + address to, + uint256 tokenId, + bytes calldata data + ) external whenTokenExists whenFromIsOwner whenToNotZeroAddress { + vm.assume(owner != ADDRESS_ZERO); + vm.assume(owner != users.alice); + vm.assume(to != ADDRESS_ZERO); + + address(facet).mint(owner, tokenId); + + vm.expectRevert( + abi.encodeWithSelector(ERC721TransferFacet.ERC721InsufficientApproval.selector, users.alice, tokenId) + ); + facet.safeTransferFrom(owner, to, tokenId, data); + } + + function testFuzz_SafeTransferFromWithData_ShouldRevert_ReceiverDoesNotImplement( + uint256 tokenId, + bytes calldata data + ) external whenTokenExists whenFromIsOwner whenToNotZeroAddress whenCallerIsAuthorized { + address(facet).mint(users.alice, tokenId); + + vm.expectRevert(abi.encodeWithSelector(ERC721TransferFacet.ERC721InvalidReceiver.selector, address(receiver))); + facet.safeTransferFrom(users.alice, address(receiver), tokenId, data); + } + + function testFuzz_SafeTransferFromWithData_ShouldRevert_ReceiverReturnsIncorrectValue( + uint256 tokenId, + bytes calldata data, + bytes4 returnedSelector + ) external whenTokenExists whenFromIsOwner whenToNotZeroAddress whenCallerIsAuthorized { + vm.assume(returnedSelector != IERC721Receiver.onERC721Received.selector); + + address(facet).mint(users.alice, tokenId); + + vm.mockCall( + address(receiver), + abi.encodeWithSelector(IERC721Receiver.onERC721Received.selector), + abi.encode(returnedSelector) + ); + + vm.expectRevert(abi.encodeWithSelector(ERC721TransferFacet.ERC721InvalidReceiver.selector, address(receiver))); + facet.safeTransferFrom(users.alice, address(receiver), tokenId, data); + } + + function testFuzz_SafeTransferFromWithData_ToEOA(address to, uint256 tokenId, bytes calldata data) + external + whenTokenExists + whenFromIsOwner + whenToNotZeroAddress + whenCallerIsAuthorized + { + vm.assume(to != ADDRESS_ZERO); + vm.assume(to != users.alice); + vm.assume(to.code.length == 0); + + address(facet).mint(users.alice, tokenId); + + uint256 senderBalanceBefore = address(facet).balanceOf(users.alice); + uint256 receiverBalanceBefore = address(facet).balanceOf(to); + + vm.expectEmit(address(facet)); + emit ERC721TransferFacet.Transfer(users.alice, to, tokenId); + facet.safeTransferFrom(users.alice, to, tokenId, data); + + assertEq(address(facet).ownerOf(tokenId), to, "ownerOf(tokenId)"); + assertEq(address(facet).balanceOf(users.alice), senderBalanceBefore - 1, "balanceOf(from)"); + assertEq(address(facet).balanceOf(to), receiverBalanceBefore + 1, "balanceOf(to)"); + assertEq(address(facet).getApproved(tokenId), ADDRESS_ZERO, "getApproved(tokenId)"); + } + + function testFuzz_SafeTransferFromWithData_ToContractWithCorrectSelector(uint256 tokenId, bytes calldata data) + external + whenTokenExists + whenFromIsOwner + whenToNotZeroAddress + whenCallerIsAuthorized + { + address(facet).mint(users.alice, tokenId); + + uint256 senderBalanceBefore = address(facet).balanceOf(users.alice); + + vm.mockCall( + address(receiver), + abi.encodeWithSelector(IERC721Receiver.onERC721Received.selector), + abi.encode(IERC721Receiver.onERC721Received.selector) + ); + + vm.expectEmit(address(facet)); + emit ERC721TransferFacet.Transfer(users.alice, address(receiver), tokenId); + facet.safeTransferFrom(users.alice, address(receiver), tokenId, data); + + assertEq(address(facet).ownerOf(tokenId), address(receiver), "ownerOf(tokenId)"); + assertEq(address(facet).balanceOf(users.alice), senderBalanceBefore - 1, "balanceOf(from)"); + assertEq(address(facet).balanceOf(address(receiver)), 1, "balanceOf(to)"); + assertEq(address(facet).getApproved(tokenId), ADDRESS_ZERO, "getApproved(tokenId)"); + } +} diff --git a/test/unit/token/ERC721/Transfer/facet/fuzz/transferFrom.t.sol b/test/unit/token/ERC721/Transfer/facet/fuzz/transferFrom.t.sol new file mode 100644 index 00000000..eafcc453 --- /dev/null +++ b/test/unit/token/ERC721/Transfer/facet/fuzz/transferFrom.t.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {ERC721TransferFacet_Base_Test} from "../ERC721TransferFacetBase.t.sol"; +import {ERC721StorageUtils} from "test/utils/storage/ERC721StorageUtils.sol"; + +import {ERC721TransferFacet} from "src/token/ERC721/Transfer/ERC721TransferFacet.sol"; + +/** + * @dev BTT spec: test/trees/ERC721.tree + */ +contract TransferFrom_ERC721TransferFacet_Fuzz_Unit_Test is ERC721TransferFacet_Base_Test { + using ERC721StorageUtils for address; + + function testFuzz_ShouldRevert_TokenDoesNotExist(address from, address to, uint256 tokenId) external { + vm.assume(to != ADDRESS_ZERO); + + vm.expectRevert(abi.encodeWithSelector(ERC721TransferFacet.ERC721NonexistentToken.selector, tokenId)); + facet.transferFrom(from, to, tokenId); + } + + function testFuzz_ShouldRevert_FromNotOwner(address from, address to, uint256 tokenId) + external + whenTokenExists + { + vm.assume(to != ADDRESS_ZERO); + vm.assume(from != ADDRESS_ZERO); + vm.assume(from != users.alice); + + address(facet).mint(users.alice, tokenId); + + vm.expectRevert( + abi.encodeWithSelector(ERC721TransferFacet.ERC721IncorrectOwner.selector, from, tokenId, users.alice) + ); + facet.transferFrom(from, to, tokenId); + } + + function testFuzz_ShouldRevert_ToIsZeroAddress(uint256 tokenId) external whenTokenExists whenFromIsOwner { + address(facet).mint(users.alice, tokenId); + + vm.expectRevert(abi.encodeWithSelector(ERC721TransferFacet.ERC721InvalidReceiver.selector, ADDRESS_ZERO)); + facet.transferFrom(users.alice, ADDRESS_ZERO, tokenId); + } + + function testFuzz_ShouldRevert_CallerNotAuthorized(address owner, address to, uint256 tokenId) + external + whenTokenExists + whenFromIsOwner + whenToNotZeroAddress + { + vm.assume(owner != ADDRESS_ZERO); + vm.assume(owner != users.alice); + vm.assume(to != ADDRESS_ZERO); + + address(facet).mint(owner, tokenId); + + vm.expectRevert( + abi.encodeWithSelector(ERC721TransferFacet.ERC721InsufficientApproval.selector, users.alice, tokenId) + ); + facet.transferFrom(owner, to, tokenId); + } + + function testFuzz_TransferFrom_CallerIsOwner(address to, uint256 tokenId, address approved) + external + whenTokenExists + whenFromIsOwner + whenToNotZeroAddress + whenCallerIsAuthorized + { + vm.assume(to != ADDRESS_ZERO); + vm.assume(to != users.alice); + + address(facet).mint(users.alice, tokenId); + address(facet).setApproved(tokenId, approved); + + uint256 senderBalanceBefore = address(facet).balanceOf(users.alice); + uint256 receiverBalanceBefore = address(facet).balanceOf(to); + + vm.expectEmit(address(facet)); + emit ERC721TransferFacet.Transfer(users.alice, to, tokenId); + facet.transferFrom(users.alice, to, tokenId); + + assertEq(address(facet).ownerOf(tokenId), to, "ownerOf(tokenId)"); + assertEq(address(facet).balanceOf(users.alice), senderBalanceBefore - 1, "balanceOf(from)"); + assertEq(address(facet).balanceOf(to), receiverBalanceBefore + 1, "balanceOf(to)"); + assertEq(address(facet).getApproved(tokenId), ADDRESS_ZERO, "getApproved(tokenId)"); + } + + function testFuzz_TransferFrom_CallerIsApprovedOperator(address owner, address to, uint256 tokenId) + external + whenTokenExists + whenFromIsOwner + whenToNotZeroAddress + whenCallerIsAuthorized + { + vm.assume(owner != ADDRESS_ZERO); + vm.assume(owner != users.alice); + vm.assume(to != ADDRESS_ZERO); + vm.assume(to != owner); + + address(facet).mint(owner, tokenId); + address(facet).setApprovalForAll(owner, users.alice, true); + + uint256 senderBalanceBefore = address(facet).balanceOf(owner); + uint256 receiverBalanceBefore = address(facet).balanceOf(to); + + vm.expectEmit(address(facet)); + emit ERC721TransferFacet.Transfer(owner, to, tokenId); + facet.transferFrom(owner, to, tokenId); + + assertEq(address(facet).ownerOf(tokenId), to, "ownerOf(tokenId)"); + assertEq(address(facet).balanceOf(owner), senderBalanceBefore - 1, "balanceOf(from)"); + assertEq(address(facet).balanceOf(to), receiverBalanceBefore + 1, "balanceOf(to)"); + assertEq(address(facet).getApproved(tokenId), ADDRESS_ZERO, "getApproved(tokenId)"); + } + + function testFuzz_TransferFrom_CallerIsTokenApproved(address owner, address to, uint256 tokenId) + external + whenTokenExists + whenFromIsOwner + whenToNotZeroAddress + whenCallerIsAuthorized + { + vm.assume(owner != ADDRESS_ZERO); + vm.assume(owner != users.alice); + vm.assume(to != ADDRESS_ZERO); + vm.assume(to != owner); + + address(facet).mint(owner, tokenId); + address(facet).setApproved(tokenId, users.alice); + + uint256 senderBalanceBefore = address(facet).balanceOf(owner); + uint256 receiverBalanceBefore = address(facet).balanceOf(to); + + vm.expectEmit(address(facet)); + emit ERC721TransferFacet.Transfer(owner, to, tokenId); + facet.transferFrom(owner, to, tokenId); + + assertEq(address(facet).ownerOf(tokenId), to, "ownerOf(tokenId)"); + assertEq(address(facet).balanceOf(owner), senderBalanceBefore - 1, "balanceOf(from)"); + assertEq(address(facet).balanceOf(to), receiverBalanceBefore + 1, "balanceOf(to)"); + assertEq(address(facet).getApproved(tokenId), ADDRESS_ZERO, "getApproved(tokenId)"); + } +} diff --git a/test/utils/Modifiers.sol b/test/utils/Modifiers.sol index 0affa72e..b10584ee 100644 --- a/test/utils/Modifiers.sol +++ b/test/utils/Modifiers.sol @@ -57,4 +57,40 @@ abstract contract Modifiers is Utils { modifier givenWhenSpenderAllowanceGETransferAmount() { _; } + + /*////////////////////////////////////////////////////////////// + ERC-721 + //////////////////////////////////////////////////////////////*/ + + modifier whenOwnerNotZeroAddress() { + _; + } + + modifier whenTokenExists() { + _; + } + + modifier whenToNotZeroAddress() { + _; + } + + modifier whenFromIsOwner() { + _; + } + + modifier whenCallerIsAuthorized() { + _; + } + + modifier whenOperatorNotZeroAddress() { + _; + } + + modifier whenRecipientNotZeroAddress() { + _; + } + + modifier givenWhenTokenDoesNotExist() { + _; + } } diff --git a/test/utils/storage/ERC721StorageUtils.sol b/test/utils/storage/ERC721StorageUtils.sol new file mode 100644 index 00000000..618f27ea --- /dev/null +++ b/test/utils/storage/ERC721StorageUtils.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/* Compose + * https://compose.diamonds + */ + +import {Vm} from "forge-std/Vm.sol"; + +/** + * @title ERC721StorageUtils + * @notice Storage manipulation utilities for ERC721 token testing + * @dev Uses vm.load and vm.store to directly manipulate storage slots + */ +library ERC721StorageUtils { + Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + bytes32 internal constant STORAGE_POSITION = keccak256("erc721"); + + /*////////////////////////////////////////////////////////////// + GETTERS + //////////////////////////////////////////////////////////////*/ + + /* + * @notice ERC-721 storage layout (ERC-8042 standard) + * @custom:storage-location erc8042:erc721 + * + * Slot 0: mapping(uint256 tokenId => address owner) ownerOf + * Slot 1: mapping(address owner => uint256 balance) balanceOf + * Slot 2: mapping(address owner => mapping(address operator => bool approved)) isApprovedForAll + * Slot 3: mapping(uint256 tokenId => address approved) approved + */ + + function ownerOf(address target, uint256 tokenId) internal view returns (address) { + bytes32 slot = keccak256(abi.encode(tokenId, uint256(STORAGE_POSITION))); + return address(uint160(uint256(vm.load(target, slot)))); + } + + function balanceOf(address target, address owner) internal view returns (uint256) { + bytes32 slot = keccak256(abi.encode(owner, uint256(STORAGE_POSITION) + 1)); + return uint256(vm.load(target, slot)); + } + + function isApprovedForAll(address target, address owner, address operator) internal view returns (bool) { + bytes32 ownerSlot = keccak256(abi.encode(owner, uint256(STORAGE_POSITION) + 2)); + bytes32 slot = keccak256(abi.encode(operator, ownerSlot)); + return uint256(vm.load(target, slot)) != 0; + } + + function getApproved(address target, uint256 tokenId) internal view returns (address) { + bytes32 slot = keccak256(abi.encode(tokenId, uint256(STORAGE_POSITION) + 3)); + return address(uint160(uint256(vm.load(target, slot)))); + } + + /*////////////////////////////////////////////////////////////// + SETTERS + //////////////////////////////////////////////////////////////*/ + + function setOwnerOf(address target, uint256 tokenId, address owner) internal { + bytes32 slot = keccak256(abi.encode(tokenId, uint256(STORAGE_POSITION))); + vm.store(target, slot, bytes32(uint256(uint160(owner)))); + } + + function setBalanceOf(address target, address owner, uint256 balance) internal { + bytes32 slot = keccak256(abi.encode(owner, uint256(STORAGE_POSITION) + 1)); + vm.store(target, slot, bytes32(balance)); + } + + function setApprovalForAll(address target, address owner, address operator, bool approved) internal { + bytes32 ownerSlot = keccak256(abi.encode(owner, uint256(STORAGE_POSITION) + 2)); + bytes32 slot = keccak256(abi.encode(operator, ownerSlot)); + vm.store(target, slot, bytes32(uint256(approved ? 1 : 0))); + } + + function setApproved(address target, uint256 tokenId, address approved) internal { + bytes32 slot = keccak256(abi.encode(tokenId, uint256(STORAGE_POSITION) + 3)); + vm.store(target, slot, bytes32(uint256(uint160(approved)))); + } + + /*////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Mint a token by setting owner and incrementing balance + */ + function mint(address target, address to, uint256 tokenId) internal { + setOwnerOf(target, tokenId, to); + uint256 currentBalance = balanceOf(target, to); + setBalanceOf(target, to, currentBalance + 1); + } + + /** + * @notice Burn a token by clearing owner and decrementing balance + */ + function burn(address target, uint256 tokenId) internal { + address owner = ownerOf(target, tokenId); + uint256 currentBalance = balanceOf(target, owner); + setOwnerOf(target, tokenId, address(0)); + setBalanceOf(target, owner, currentBalance - 1); + setApproved(target, tokenId, address(0)); + } +}