diff --git a/src/test/tokenerc1155-BTT/burn-batch/burnBatch.t.sol b/src/test/tokenerc1155-BTT/burn-batch/burnBatch.t.sol new file mode 100644 index 000000000..273cb2db4 --- /dev/null +++ b/src/test/tokenerc1155-BTT/burn-batch/burnBatch.t.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_BurnBatch is BaseTest { + address public implementation; + address public proxy; + address public caller; + address public recipient; + string public uri; + uint256 public amount; + + MyTokenERC1155 internal tokenContract; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + caller = getActor(1); + recipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + uri = "uri"; + amount = 100; + + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), caller); + } + + function test_burn_whenNotOwnerNorApproved() public { + // mint two tokenIds + vm.startPrank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + vm.stopPrank(); + + uint256[] memory ids = new uint256[](2); + uint256[] memory amounts = new uint256[](2); + + ids[0] = 0; + ids[1] = 1; + amounts[0] = 10; + amounts[1] = 10; + + // burn + vm.expectRevert("ERC1155: caller is not owner nor approved."); + tokenContract.burnBatch(recipient, ids, amounts); + } + + function test_burn_whenOwner_invalidAmount() public { + // mint two tokenIds + vm.startPrank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + vm.stopPrank(); + + uint256[] memory ids = new uint256[](2); + uint256[] memory amounts = new uint256[](2); + + ids[0] = 0; + ids[1] = 1; + amounts[0] = 1000 ether; + amounts[1] = 10; + + // burn + vm.prank(recipient); + vm.expectRevert(); + tokenContract.burnBatch(recipient, ids, amounts); + } + + function test_burn_whenOwner() public { + // mint two tokenIds + vm.startPrank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + vm.stopPrank(); + + uint256[] memory ids = new uint256[](2); + uint256[] memory amounts = new uint256[](2); + + ids[0] = 0; + ids[1] = 1; + amounts[0] = 10; + amounts[1] = 10; + + // burn + vm.prank(recipient); + tokenContract.burnBatch(recipient, ids, amounts); + + assertEq(tokenContract.balanceOf(recipient, ids[0]), amount - amounts[0]); + assertEq(tokenContract.balanceOf(recipient, ids[1]), amount - amounts[1]); + } + + function test_burn_whenApproved() public { + // mint two tokenIds + vm.startPrank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + vm.stopPrank(); + + uint256[] memory ids = new uint256[](2); + uint256[] memory amounts = new uint256[](2); + + ids[0] = 0; + ids[1] = 1; + amounts[0] = 10; + amounts[1] = 10; + + vm.prank(recipient); + tokenContract.setApprovalForAll(caller, true); + + // burn + vm.prank(caller); + tokenContract.burnBatch(recipient, ids, amounts); + + assertEq(tokenContract.balanceOf(recipient, ids[0]), amount - amounts[0]); + assertEq(tokenContract.balanceOf(recipient, ids[1]), amount - amounts[1]); + } +} diff --git a/src/test/tokenerc1155-BTT/burn-batch/burnBatch.tree b/src/test/tokenerc1155-BTT/burn-batch/burnBatch.tree new file mode 100644 index 000000000..dca6fa537 --- /dev/null +++ b/src/test/tokenerc1155-BTT/burn-batch/burnBatch.tree @@ -0,0 +1,14 @@ +burnBatch( + address account, + uint256[] memory ids, + uint256[] memory values +) +├── when the caller isn't `account` or `account` hasn't approved tokens to caller +│ └── it should revert ✅ +└── when the caller is `account` with balances less than `values` for corresponding `ids` +│ └── it should revert ✅ +└── when the caller is `account` with balances greater than or equal to `values` +│ └── it should burn `values` amounts of `ids` tokens from account ✅ +└── when the `account` has approved `values` amount of tokens to caller + └── it should burn the token ✅ + diff --git a/src/test/tokenerc1155-BTT/burn/burn.t.sol b/src/test/tokenerc1155-BTT/burn/burn.t.sol new file mode 100644 index 000000000..1bf2575b9 --- /dev/null +++ b/src/test/tokenerc1155-BTT/burn/burn.t.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_Burn is BaseTest { + address public implementation; + address public proxy; + address public caller; + address public recipient; + string public uri; + uint256 public amount; + + MyTokenERC1155 internal tokenContract; + + event MetadataUpdate(uint256 _tokenId); + event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + caller = getActor(1); + recipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + uri = "uri"; + amount = 100; + + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), caller); + } + + function test_burn_whenNotOwnerNorApproved() public { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + + // burn + vm.expectRevert("ERC1155: caller is not owner nor approved."); + tokenContract.burn(recipient, _tokenIdToMint, amount); + } + + function test_burn_whenOwner_invalidAmount() public { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + + // burn + vm.prank(recipient); + vm.expectRevert(); + tokenContract.burn(recipient, _tokenIdToMint, amount + 1); + } + + function test_burn_whenOwner() public { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + + // burn + vm.prank(recipient); + tokenContract.burn(recipient, _tokenIdToMint, amount); + + assertEq(tokenContract.balanceOf(recipient, _tokenIdToMint), 0); + } + + function test_burn_whenApproved() public { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + + vm.prank(recipient); + tokenContract.setApprovalForAll(caller, true); + + // burn + vm.prank(caller); + tokenContract.burn(recipient, _tokenIdToMint, amount); + + assertEq(tokenContract.balanceOf(recipient, _tokenIdToMint), 0); + } +} diff --git a/src/test/tokenerc1155-BTT/burn/burn.tree b/src/test/tokenerc1155-BTT/burn/burn.tree new file mode 100644 index 000000000..8232a832d --- /dev/null +++ b/src/test/tokenerc1155-BTT/burn/burn.tree @@ -0,0 +1,14 @@ +burn( + address account, + uint256 id, + uint256 value +) +├── when the caller isn't `account` or `account` hasn't approved tokens to caller +│ └── it should revert ✅ +└── when the caller is `account` with balance less than `value` +│ └── it should revert ✅ +└── when the caller is `account` with balance greater than or equal to `value` +│ └── it should burn `value` amount of `id` tokens from ✅ +└── when the `account` has approved `value` amount of tokens to caller + └── it should burn the token ✅ + diff --git a/src/test/tokenerc1155-BTT/initialize/initialize.t.sol b/src/test/tokenerc1155-BTT/initialize/initialize.t.sol new file mode 100644 index 000000000..5340ff595 --- /dev/null +++ b/src/test/tokenerc1155-BTT/initialize/initialize.t.sol @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { IPlatformFee } from "contracts/extension/interface/IPlatformFee.sol"; + +contract MyTokenERC1155 is TokenERC1155 { + function eip712NameHash() external view returns (bytes32) { + return _EIP712NameHash(); + } + + function eip712VersionHash() external view returns (bytes32) { + return _EIP712VersionHash(); + } +} + +contract TokenERC1155Test_Initialize is BaseTest { + address public implementation; + address public proxy; + + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + } + + function test_initialize_initializingImplementation() public { + vm.expectRevert("Initializable: contract is already initialized"); + TokenERC1155(implementation).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + modifier whenNotImplementation() { + _; + } + + function test_initialize_proxyAlreadyInitialized() public whenNotImplementation { + vm.expectRevert("Initializable: contract is already initialized"); + MyTokenERC1155(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + modifier whenProxyNotInitialized() { + proxy = address(new TWProxy(implementation, "")); + _; + } + + function test_initialize_exceedsMaxBps() public whenNotImplementation whenProxyNotInitialized { + vm.expectRevert("exceeds MAX_BPS"); + MyTokenERC1155(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + uint128(MAX_BPS) + 1, // platformFeeBps greater than MAX_BPS + platformFeeRecipient + ); + } + + modifier whenPlatformFeeBpsWithinMaxBps() { + _; + } + + function test_initialize() public whenNotImplementation whenProxyNotInitialized whenPlatformFeeBpsWithinMaxBps { + MyTokenERC1155(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + + // check state + MyTokenERC1155 tokenContract = MyTokenERC1155(proxy); + + assertEq(tokenContract.eip712NameHash(), keccak256(bytes("TokenERC1155"))); + assertEq(tokenContract.eip712VersionHash(), keccak256(bytes("1"))); + + address[] memory _trustedForwarders = forwarders(); + for (uint256 i = 0; i < _trustedForwarders.length; i++) { + assertTrue(tokenContract.isTrustedForwarder(_trustedForwarders[i])); + } + + assertEq(tokenContract.name(), NAME); + assertEq(tokenContract.symbol(), SYMBOL); + assertEq(tokenContract.contractURI(), CONTRACT_URI); + + (address _platformFeeRecipient, uint16 _platformFeeBps) = tokenContract.getPlatformFeeInfo(); + assertEq(_platformFeeBps, platformFeeBps); + assertEq(_platformFeeRecipient, platformFeeRecipient); + assertEq(tokenContract.platformFeeRecipient(), platformFeeRecipient); + assertEq(uint8(tokenContract.getPlatformFeeType()), uint8(IPlatformFee.PlatformFeeType.Bps)); + + (address _royaltyRecipient, uint16 _royaltyBps) = tokenContract.getDefaultRoyaltyInfo(); + (address _royaltyRecipientForToken, uint16 _royaltyBpsForToken) = tokenContract.getRoyaltyInfoForToken(1); // random tokenId + assertEq(_royaltyBps, royaltyBps); + assertEq(_royaltyRecipient, royaltyRecipient); + assertEq(_royaltyRecipient, _royaltyRecipientForToken); + assertEq(_royaltyBps, _royaltyBpsForToken); + + assertEq(tokenContract.primarySaleRecipient(), saleRecipient); + + assertEq(tokenContract.owner(), deployer); + assertTrue(tokenContract.hasRole(bytes32(0x00), deployer)); + assertTrue(tokenContract.hasRole(keccak256("TRANSFER_ROLE"), deployer)); + assertTrue(tokenContract.hasRole(keccak256("TRANSFER_ROLE"), address(0))); + assertTrue(tokenContract.hasRole(keccak256("MINTER_ROLE"), deployer)); + assertTrue(tokenContract.hasRole(keccak256("METADATA_ROLE"), deployer)); + assertEq(tokenContract.getRoleAdmin(keccak256("METADATA_ROLE")), keccak256("METADATA_ROLE")); + } + + function test_initialize_event_RoleGranted_DefaultAdmin() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _defaultAdminRole = bytes32(0x00); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_defaultAdminRole, deployer, deployer); + MyTokenERC1155(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_MinterRole() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _minterRole = keccak256("MINTER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_minterRole, deployer, deployer); + MyTokenERC1155(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_TransferRole() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_transferRole, deployer, deployer); + MyTokenERC1155(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_TransferRole_AddressZero() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_transferRole, address(0), deployer); + MyTokenERC1155(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_MetadataRole() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _metadataRole = keccak256("METADATA_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_metadataRole, deployer, deployer); + MyTokenERC1155(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleAdminChanged_MetadataRole() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _metadataRole = keccak256("METADATA_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleAdminChanged(_metadataRole, bytes32(0x00), _metadataRole); + MyTokenERC1155(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } +} diff --git a/src/test/tokenerc1155-BTT/initialize/initialize.tree b/src/test/tokenerc1155-BTT/initialize/initialize.tree new file mode 100644 index 000000000..15c2ba936 --- /dev/null +++ b/src/test/tokenerc1155-BTT/initialize/initialize.tree @@ -0,0 +1,43 @@ +initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _primarySaleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient +) +├── when initializing the implementation contract (not proxy) +│ └── it should revert ✅ +└── when it is a proxy to the implementation + └── when it is already initialized + │ └── it should revert ✅ + └── when it is not initialized + └── when platformFeeBps is greater than MAX_BPS + │ └── it should revert ✅ + └── when platformFeeBps is less than or equal to MAX_BPS + └── it should correctly set EIP712 name hash and version hash ✅ + └── it should set trustedForwarder mapping to true for all addresses in `_trustedForwarders` ✅ + └── it should set name and symbol to `_name` and `_symbol` param values respectively ✅ + └── it should set contractURI to `_contractURI` param value ✅ + └── it should set platformFeeRecipient and platformFeeBps as `_platformFeeRecipient` and `_platformFeeBps` respectively ✅ + └── it should set platformFeeType to `Bps` ✅ + └── it should set royaltyRecipient and royaltyBps as `_royaltyRecipient` and `_royaltyBps` respectively ✅ + └── it should set primary sale recipient as `_primarySaleRecipient` param value ✅ + └── it should set _owner to `_defaultAdmin` param value ✅ + └── it should grant 0x00 (DEFAULT_ADMIN_ROLE) to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should grant MINTER_ROLE to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should grant TRANSFER_ROLE to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should grant TRANSFER_ROLE to address(0) ✅ + └── it should emit RoleGranted event ✅ + └── it should grant METADATA_ROLE to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should set METADATA_ROLE as role admin for METADATA_ROLE ✅ + └── it should emit RoleAdminChanged event ✅ + diff --git a/src/test/tokenerc1155-BTT/mint-to/mintTo.t.sol b/src/test/tokenerc1155-BTT/mint-to/mintTo.t.sol new file mode 100644 index 000000000..6877e56c1 --- /dev/null +++ b/src/test/tokenerc1155-BTT/mint-to/mintTo.t.sol @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract ERC1155ReceiverCompliant is IERC1155Receiver { + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external view virtual override returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4) { + return this.onERC1155BatchReceived.selector; + } + + function supportsInterface(bytes4 interfaceId) external view returns (bool) {} +} + +contract TokenERC1155Test_MintTo is BaseTest { + address public implementation; + address public proxy; + address public caller; + address public recipient; + string public uri; + uint256 public amount; + + MyTokenERC1155 internal tokenContract; + ERC1155ReceiverCompliant internal erc1155ReceiverContract; + + event MetadataUpdate(uint256 _tokenId); + event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri, uint256 quantityMinted); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + erc1155ReceiverContract = new ERC1155ReceiverCompliant(); + caller = getActor(1); + recipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + amount = 100; + uri = "ipfs://uri"; + } + + function test_mintTo_notMinterRole() public { + vm.prank(caller); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + TWStrings.toHexString(uint160(caller), 20), + " is missing role ", + TWStrings.toHexString(uint256(keccak256("MINTER_ROLE")), 32) + ) + ); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + } + + modifier whenMinterRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), caller); + _; + } + + // ================== + // ======= Test branch: `tokenId` input param is type(uint256).max + // ================== + + function test_mintTo_maxTokenId_EOA() public whenMinterRole { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + + // check state after + assertEq(_tokenIdToMint, 0); + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(recipient, _tokenIdToMint), amount); + assertEq(tokenContract.uri(_tokenIdToMint), uri); + } + + function test_mintTo_maxTokenId_EOA_TokensMintedEvent() public whenMinterRole { + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMinted(recipient, _tokenIdToMint, uri, amount); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + } + + function test_mintTo_maxTokenId_EOA_MetadataUpdateEvent() public whenMinterRole { + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(false, false, false, true); + emit MetadataUpdate(_tokenIdToMint); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + } + + function test_mintTo_maxTokenId_EOA_uriAlreadyPresent() public whenMinterRole { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(deployer); + tokenContract.setTokenURI(_tokenIdToMint, "ipfs://uriOld"); + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + + // check state after + assertEq(_tokenIdToMint, 0); + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(recipient, _tokenIdToMint), amount); + assertEq(tokenContract.uri(_tokenIdToMint), "ipfs://uriOld"); + } + + function test_mintTo_maxTokenId_nonERC1155ReceiverContract() public whenMinterRole { + recipient = address(this); + vm.prank(caller); + vm.expectRevert(); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + } + + modifier whenERC1155Receiver() { + recipient = address(erc1155ReceiverContract); + _; + } + + function test_mintTo_maxTokenId_contract() public whenMinterRole whenERC1155Receiver { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + + // check state after + assertEq(_tokenIdToMint, 0); + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(recipient, _tokenIdToMint), amount); + assertEq(tokenContract.uri(_tokenIdToMint), uri); + } + + function test_mintTo_maxTokenId_contract_TokensMintedEvent() public whenMinterRole whenERC1155Receiver { + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMinted(recipient, _tokenIdToMint, uri, amount); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + } + + function test_mintTo_maxTokenId_contract_MetadataUpdateEvent() public whenMinterRole whenERC1155Receiver { + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(false, false, false, true); + emit MetadataUpdate(_tokenIdToMint); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + } + + function test_mintTo_maxTokenId_contract_uriAlreadyPresent() public whenMinterRole whenERC1155Receiver { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(deployer); + tokenContract.setTokenURI(_tokenIdToMint, "ipfs://uriOld"); + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, type(uint256).max, uri, amount); + + // check state after + assertEq(_tokenIdToMint, 0); + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(recipient, _tokenIdToMint), amount); + assertEq(tokenContract.uri(_tokenIdToMint), "ipfs://uriOld"); + } + + // ================== + // ======= Test branch: `tokenId` input param is not type(uint256).max + // ================== + + modifier whenNotMaxTokenId() { + // pre-mint the first token (i.e. id 0), so that nextTokenIdToMint is 1, for this code path + vm.prank(deployer); + tokenContract.mintTo(deployer, type(uint256).max, "uri1", amount); + _; + } + + function test_mintTo_EOA_invalidId() public whenMinterRole whenNotMaxTokenId { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + vm.expectRevert("invalid id"); + tokenContract.mintTo(recipient, _tokenIdToMint, uri, amount); + } + + modifier whenValidId() { + _; + } + + function test_mintTo_EOA() public whenMinterRole whenNotMaxTokenId whenValidId { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint() - 1; + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, _tokenIdToMint, uri, amount); + + // check state after + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(recipient, _tokenIdToMint), amount); + assertEq(tokenContract.uri(_tokenIdToMint), "uri1"); + } + + function test_mintTo_EOA_TokensMintedEvent() public whenMinterRole whenNotMaxTokenId whenValidId { + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint() - 1; + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMinted(recipient, _tokenIdToMint, "uri1", amount); + tokenContract.mintTo(recipient, _tokenIdToMint, uri, amount); + } + + function test_mintTo_nonERC1155ReceiverContract() public whenMinterRole whenNotMaxTokenId whenValidId { + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint() - 1; + + recipient = address(this); + vm.prank(caller); + vm.expectRevert(); + tokenContract.mintTo(recipient, _tokenIdToMint, uri, amount); + } + + function test_mintTo_contract() public whenMinterRole whenNotMaxTokenId whenERC1155Receiver whenValidId { + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint() - 1; + + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, _tokenIdToMint, uri, amount); + + // check state after + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(recipient, _tokenIdToMint), amount); + assertEq(tokenContract.uri(_tokenIdToMint), "uri1"); + } + + function test_mintTo_contract_TokensMintedEvent() + public + whenMinterRole + whenNotMaxTokenId + whenERC1155Receiver + whenValidId + { + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint() - 1; + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMinted(recipient, _tokenIdToMint, "uri1", amount); + tokenContract.mintTo(recipient, _tokenIdToMint, uri, amount); + } +} diff --git a/src/test/tokenerc1155-BTT/mint-to/mintTo.tree b/src/test/tokenerc1155-BTT/mint-to/mintTo.tree new file mode 100644 index 000000000..facd1e8eb --- /dev/null +++ b/src/test/tokenerc1155-BTT/mint-to/mintTo.tree @@ -0,0 +1,48 @@ +mintTo( + address _to, + uint256 _tokenId, + string calldata _uri, + uint256 _amount +) +├── when caller doesn't have MINTER_ROLE + │ └── it should revert ✅ + └── when caller has MINTER_ROLE + ├── when `_tokenId` is type(uint256).max + │ ├── when `_to` address is an EOA + │ │ └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + │ │ └── it should increment `nextTokenIdToMint` by 1 ✅ + │ │ └── it should mint the `_amount` number of tokens to the `_to` address ✅ + │ │ └── it should emit TokensMinted event ✅ + │ │ └── when there is no uri associated with the minted tokenId + │ │ └── it should set uri for minted tokenId equal to `_uri` ✅ + │ │ └── it should emit MetadataUpdate event ✅ + │ └── when `_to` address is a contract + │ ├── when `_to` address is non ERC1155Receiver implementer + │ │ └── it should revert ✅ + │ └── when `_to` address implements ERC1155Receiver + │ └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + │ └── it should increment `nextTokenIdToMint` by 1 ✅ + │ └── it should mint the `_amount` number of tokens to the `_to` address ✅ + │ └── it should emit TokensMinted event ✅ + │ └── when there is no uri associated with the minted tokenId + │ └── it should set uri for minted tokenId equal to `_uri` ✅ + │ └── it should emit MetadataUpdate event ✅ + │ + └── when `_tokenId` is not type(uint256).max + ├── when `_tokenId` is not less than nextTokenIdToMint + │ └── it should revert ✅ + └── when `_tokenId` is less than nextTokenIdToMint + ├── when `_to` address is an EOA + │ └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + │ └── it should increment `nextTokenIdToMint` by 1 ✅ + │ └── it should mint the `_amount` number of tokens to the `_to` address ✅ + │ └── it should emit TokensMinted event ✅ + └── when `_to` address is a contract + ├── when `_to` address is non ERC1155Receiver implementer + │ └── it should revert ✅ + └── when `_to` address implements ERC1155Receiver + └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + └── it should increment `nextTokenIdToMint` by 1 ✅ + └── it should mint the `_amount` number of tokens to the `_to` address ✅ + └── it should emit TokensMinted event ✅ + diff --git a/src/test/tokenerc1155-BTT/mint-with-signature/mintWithSignature.t.sol b/src/test/tokenerc1155-BTT/mint-with-signature/mintWithSignature.t.sol new file mode 100644 index 000000000..83c039706 --- /dev/null +++ b/src/test/tokenerc1155-BTT/mint-with-signature/mintWithSignature.t.sol @@ -0,0 +1,895 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { IPlatformFee } from "contracts/extension/interface/IPlatformFee.sol"; + +contract MyTokenERC1155 is TokenERC1155 { + function setMintedURI(MintRequest calldata _req, bytes calldata _signature) external { + verifyRequest(_req, _signature); + } +} + +contract ERC1155ReceiverCompliant is IERC1155Receiver { + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external view virtual override returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4) { + return this.onERC1155BatchReceived.selector; + } + + function supportsInterface(bytes4 interfaceId) external view returns (bool) {} +} + +contract ReentrantContract { + fallback() external payable { + TokenERC1155.MintRequest memory _mintrequest; + bytes memory _signature; + MyTokenERC1155(msg.sender).mintWithSignature(_mintrequest, _signature); + } +} + +contract TokenERC1155Test_MintWithSignature is BaseTest { + address public implementation; + address public proxy; + address public caller; + address public recipient; + string public uri; + + MyTokenERC1155 internal tokenContract; + ERC1155ReceiverCompliant internal erc1155ReceiverContract; + + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + TokenERC1155.MintRequest _mintrequest; + + event MetadataUpdate(uint256 _tokenId); + event TokensMinted(address indexed mintedTo, uint256 indexed tokenIdMinted, string uri); + event TokensMintedWithSignature( + address indexed signer, + address indexed mintedTo, + uint256 indexed tokenIdMinted, + TokenERC1155.MintRequest mintRequest + ); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + erc1155ReceiverContract = new ERC1155ReceiverCompliant(); + caller = getActor(1); + recipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + + typehashMintRequest = keccak256( + "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,uint256 tokenId,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("TokenERC1155")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tokenContract)) + ); + + // construct default mintrequest + _mintrequest.to = address(0x1234); + _mintrequest.royaltyRecipient = royaltyRecipient; + _mintrequest.royaltyBps = royaltyBps; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.tokenId = type(uint256).max; + _mintrequest.uri = "ipfs://"; + _mintrequest.quantity = 100; + _mintrequest.pricePerToken = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 0; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + erc20.mint(caller, 1_000 ether); + vm.deal(caller, 1_000 ether); + + vm.startPrank(deployer); + erc20.approve(address(tokenContract), type(uint256).max); + vm.stopPrank(); + + vm.startPrank(caller); + erc20.approve(address(tokenContract), type(uint256).max); + vm.stopPrank(); + } + + function signMintRequest(TokenERC1155.MintRequest memory _request, uint256 _privateKey) + internal + view + returns (bytes memory) + { + bytes memory encodedRequest = bytes.concat( + abi.encode( + typehashMintRequest, + _request.to, + _request.royaltyRecipient, + _request.royaltyBps, + _request.primarySaleRecipient, + _request.tokenId, + keccak256(bytes(_request.uri)) + ), + abi.encode( + _request.quantity, + _request.pricePerToken, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ) + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + // ================== + // ======= Assume _req.tokenId input is type(uint256).max and platform fee type is Bps + // ================== + + function test_mintWithSignature_notMinterRole() public { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + vm.expectRevert("invalid signature"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenMinterRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), signer); + _; + } + + function test_mintWithSignature_invalidUID() public whenMinterRole { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // set state with this mintrequest and signature, marking the UID as used + tokenContract.setMintedURI(_mintrequest, _signature); + + // pass the same UID mintrequest again + vm.prank(caller); + vm.expectRevert("invalid signature"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenUidNotUsed() { + _; + } + + function test_mintWithSignature_invalidStartTimestamp() public whenMinterRole whenUidNotUsed { + _mintrequest.validityStartTimestamp = uint128(block.timestamp + 1); + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("request expired"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenValidStartTimestamp() { + _; + } + + function test_mintWithSignature_invalidEndTimestamp() public whenMinterRole whenUidNotUsed whenValidStartTimestamp { + _mintrequest.validityEndTimestamp = uint128(block.timestamp - 1); + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("request expired"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenValidEndTimestamp() { + _; + } + + function test_mintWithSignature_recipientAddressZero() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + { + _mintrequest.to = address(0); + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("recipient undefined"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenRecipientAddressNotZero() { + _; + } + + function test_mintWithSignature_zeroQuantity() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + { + _mintrequest.quantity = 0; + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("zero quantity"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenNotZeroQuantity() { + _mintrequest.quantity = 100; + _; + } + + // ================== + // ======= Test branch: when mint price is zero + // ================== + + function test_mintWithSignature_zeroPrice_msgValueNonZero() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + { + _mintrequest.pricePerToken = 0; + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("!Value"); + vm.prank(caller); + tokenContract.mintWithSignature{ value: 1 }(_mintrequest, _signature); + } + + modifier whenMsgValueZero() { + _; + } + + function test_mintWithSignature_zeroPrice_EOA() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + + // check state after + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(_mintrequest.to, _tokenIdToMint), _mintrequest.quantity); + assertEq(tokenContract.uri(_tokenIdToMint), _mintrequest.uri); + assertEq(tokenContract.totalSupply(_tokenIdToMint), _mintrequest.quantity); + } + + function test_mintWithSignature_zeroPrice_EOA_MetadataUpdateEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(false, false, false, true); + emit MetadataUpdate(_tokenIdToMint); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_mintWithSignature_zeroPrice_EOA_TokensMintedWithSignatureEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMintedWithSignature(signer, _mintrequest.to, _tokenIdToMint, _mintrequest); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_mintWithSignature_zeroPrice_nonERC1155ReceiverContract() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 0; + _mintrequest.to = address(this); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // mint + vm.prank(caller); + vm.expectRevert(); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenERC1155Receiver() { + _mintrequest.to = address(erc1155ReceiverContract); + _; + } + + function test_mintWithSignature_zeroPrice_contract() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + whenERC1155Receiver + { + _mintrequest.pricePerToken = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + // mint + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + + // check state after + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(_mintrequest.to, _tokenIdToMint), _mintrequest.quantity); + assertEq(tokenContract.uri(_tokenIdToMint), _mintrequest.uri); + assertEq(tokenContract.totalSupply(_tokenIdToMint), _mintrequest.quantity); + } + + function test_mintWithSignature_zeroPrice_contract_MetadataUpdateEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + whenERC1155Receiver + { + _mintrequest.pricePerToken = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(false, false, false, true); + emit MetadataUpdate(_tokenIdToMint); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_mintWithSignature_zeroPrice_contract_TokensMintedWithSignatureEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + whenERC1155Receiver + { + _mintrequest.pricePerToken = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensMintedWithSignature(signer, _mintrequest.to, _tokenIdToMint, _mintrequest); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + // ================== + // ======= Test branch: when mint price is not zero + // ================== + + function test_mintWithSignature_nonZeroPrice_nativeToken_incorrectMsgValue() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + { + _mintrequest.pricePerToken = 10; + _mintrequest.currency = NATIVE_TOKEN; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 incorrectTotalPrice = (_mintrequest.pricePerToken * _mintrequest.quantity) + 1; + + vm.expectRevert("must send total price."); + vm.prank(caller); + tokenContract.mintWithSignature{ value: incorrectTotalPrice }(_mintrequest, _signature); + } + + modifier whenCorrectMsgValue() { + _; + } + + function test_mintWithSignature_nonZeroPrice_nativeToken() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenCorrectMsgValue + { + _mintrequest.pricePerToken = 10; + _mintrequest.currency = NATIVE_TOKEN; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + uint256 totalPrice = (_mintrequest.pricePerToken * _mintrequest.quantity); + + // mint + vm.prank(caller); + tokenContract.mintWithSignature{ value: totalPrice }(_mintrequest, _signature); + + // check state after + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(_mintrequest.to, _tokenIdToMint), _mintrequest.quantity); + assertEq(tokenContract.uri(_tokenIdToMint), _mintrequest.uri); + assertEq(tokenContract.totalSupply(_tokenIdToMint), _mintrequest.quantity); + + uint256 _platformFee = (totalPrice * platformFeeBps) / 10_000; + uint256 _saleProceeds = totalPrice - _platformFee; + assertEq(caller.balance, 1000 ether - totalPrice); + assertEq(tokenContract.platformFeeRecipient().balance, _platformFee); + assertEq(tokenContract.primarySaleRecipient().balance, _saleProceeds); + } + + function test_mintWithSignature_nonZeroPrice_nativeToken_MetadataUpdateEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenCorrectMsgValue + { + _mintrequest.pricePerToken = 10; + _mintrequest.currency = NATIVE_TOKEN; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(false, false, false, true); + emit MetadataUpdate(_tokenIdToMint); + tokenContract.mintWithSignature{ value: _mintrequest.pricePerToken * _mintrequest.quantity }( + _mintrequest, + _signature + ); + } + + function test_mintWithSignature_nonZeroPrice_nativeToken_TokensMintedWithSignatureEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenCorrectMsgValue + { + _mintrequest.pricePerToken = 10; + _mintrequest.currency = NATIVE_TOKEN; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensMintedWithSignature(signer, _mintrequest.to, _tokenIdToMint, _mintrequest); + tokenContract.mintWithSignature{ value: _mintrequest.pricePerToken * _mintrequest.quantity }( + _mintrequest, + _signature + ); + } + + function test_mintWithSignature_nonZeroPrice_ERC20_nonZeroMsgValue() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + { + _mintrequest.pricePerToken = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("msg value not zero"); + vm.prank(caller); + tokenContract.mintWithSignature{ value: 1 }(_mintrequest, _signature); + } + + function test_mintWithSignature_nonZeroPrice_ERC20() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + uint256 totalPrice = (_mintrequest.pricePerToken * _mintrequest.quantity); + + // mint + vm.prank(caller); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + + // check state after + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(_mintrequest.to, _tokenIdToMint), _mintrequest.quantity); + assertEq(tokenContract.uri(_tokenIdToMint), _mintrequest.uri); + assertEq(tokenContract.totalSupply(_tokenIdToMint), _mintrequest.quantity); + + uint256 _platformFee = (totalPrice * platformFeeBps) / 10_000; + uint256 _saleProceeds = totalPrice - _platformFee; + assertEq(erc20.balanceOf(caller), 1000 ether - totalPrice); + assertEq(erc20.balanceOf(tokenContract.platformFeeRecipient()), _platformFee); + assertEq(erc20.balanceOf(tokenContract.primarySaleRecipient()), _saleProceeds); + } + + function test_mintWithSignature_nonZeroPrice_ERC20_MetadataUpdateEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(false, false, false, true); + emit MetadataUpdate(_tokenIdToMint); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + function test_mintWithSignature_nonZeroPrice_ERC20_TokensMintedWithSignatureEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensMintedWithSignature(signer, _mintrequest.to, _tokenIdToMint, _mintrequest); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + // ================== + // ======= Test branch: other cases + // ================== + + function test_mintWithSignature_nonZeroRoyaltyRecipient() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + + (address _royaltyRecipient, uint16 _royaltyBps) = tokenContract.getRoyaltyInfoForToken(0); + assertEq(_royaltyRecipient, royaltyRecipient); + assertEq(_royaltyBps, royaltyBps); + } + + function test_mintWithSignature_royaltyRecipientZeroAddress() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 0; + _mintrequest.royaltyRecipient = address(0); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + + (address _royaltyRecipient, uint16 _royaltyBps) = tokenContract.getRoyaltyInfoForToken(0); + (address _defaultRoyaltyRecipient, uint16 _defaultRoyaltyBps) = tokenContract.getDefaultRoyaltyInfo(); + assertEq(_royaltyRecipient, _defaultRoyaltyRecipient); + assertEq(_royaltyBps, _defaultRoyaltyBps); + } + + function test_mintWithSignature_reentrantRecipientContract() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.pricePerToken = 0; + _mintrequest.to = address(new ReentrantContract()); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + vm.expectRevert("ReentrancyGuard: reentrant call"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_mintWithSignature_nonZeroPrice_flatFee_exceedsTotalPrice() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + vm.startPrank(deployer); + tokenContract.setPlatformFeeType(IPlatformFee.PlatformFeeType.Flat); + tokenContract.setFlatPlatformFeeInfo(platformFeeRecipient, 100 ether); + vm.stopPrank(); + + _mintrequest.pricePerToken = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + uint256 totalPrice = (_mintrequest.pricePerToken * _mintrequest.quantity); + + // mint + vm.prank(caller); + vm.expectRevert("price less than platform fee"); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + function test_mintWithSignature_nonZeroPrice_flatFee() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + vm.prank(deployer); + tokenContract.setPlatformFeeType(IPlatformFee.PlatformFeeType.Flat); + + _mintrequest.pricePerToken = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint(); + + uint256 totalPrice = (_mintrequest.pricePerToken * _mintrequest.quantity); + + // mint + vm.prank(caller); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + + // check state after + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(_mintrequest.to, _tokenIdToMint), _mintrequest.quantity); + assertEq(tokenContract.uri(_tokenIdToMint), _mintrequest.uri); + assertEq(tokenContract.totalSupply(_tokenIdToMint), _mintrequest.quantity); + + (, uint256 _platformFee) = tokenContract.getFlatPlatformFeeInfo(); + uint256 _saleProceeds = totalPrice - _platformFee; + assertEq(erc20.balanceOf(caller), 1000 ether - totalPrice); + assertEq(erc20.balanceOf(tokenContract.platformFeeRecipient()), _platformFee); + assertEq(erc20.balanceOf(tokenContract.primarySaleRecipient()), _saleProceeds); + } + + modifier whenNotMaxTokenId() { + // pre-mint the first token (i.e. id 0), so that nextTokenIdToMint is 1, for this code path + vm.prank(deployer); + tokenContract.mintTo(deployer, type(uint256).max, "uri1", 10); + _; + } + + function test_mintWithSignature_nonZeroPrice_notMaxTokenId_invalidId() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + whenNotMaxTokenId + { + vm.prank(deployer); + tokenContract.setPlatformFeeType(IPlatformFee.PlatformFeeType.Flat); + + _mintrequest.pricePerToken = 10; + _mintrequest.currency = address(erc20); + _mintrequest.tokenId = 1; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + vm.expectRevert("invalid id"); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + modifier whenValidId() { + _; + } + + function test_mintWithSignature_nonZeroPrice_notMaxTokenId() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + whenNotMaxTokenId + whenValidId + { + vm.prank(deployer); + tokenContract.setPlatformFeeType(IPlatformFee.PlatformFeeType.Flat); + + _mintrequest.pricePerToken = 10; + _mintrequest.currency = address(erc20); + _mintrequest.tokenId = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // state before + uint256 _tokenIdToMint = tokenContract.nextTokenIdToMint() - 1; + + uint256 totalPrice = (_mintrequest.pricePerToken * _mintrequest.quantity); + + // mint + vm.prank(caller); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + + // check state after + assertEq(tokenContract.nextTokenIdToMint(), _tokenIdToMint + 1); + assertEq(tokenContract.balanceOf(_mintrequest.to, _tokenIdToMint), _mintrequest.quantity); + assertEq(tokenContract.uri(_tokenIdToMint), "uri1"); + assertEq(tokenContract.totalSupply(_tokenIdToMint), _mintrequest.quantity + 10); + } +} diff --git a/src/test/tokenerc1155-BTT/mint-with-signature/mintWithSignature.tree b/src/test/tokenerc1155-BTT/mint-with-signature/mintWithSignature.tree new file mode 100644 index 000000000..115264aec --- /dev/null +++ b/src/test/tokenerc1155-BTT/mint-with-signature/mintWithSignature.tree @@ -0,0 +1,102 @@ +mintWithSignature(MintRequest calldata _req, bytes calldata _signature) +// assuming _req.tokenId input is type(uint256).max and platform fee type is Bps +├── when signer doesn't have MINTER_ROLE +│ └── it should revert ✅ +└── when signer has MINTER_ROLE + └── when `_req.uid` has already been used + │ └── it should revert ✅ + └── when `_req.uid` has not been used + └── when `_req.validityStartTimestamp` is greater than block timestamp + │ └── it should revert ✅ + └── when `_req.validityStartTimestamp` is less than or equal to block timestamp + └── when `_req.validityEndTimestamp` is less than block timestamp + │ └── it should revert ✅ + └── when `_req.validityEndTimestamp` is greater than or equal to block timestamp + └── when `_req.to` is address(0) + │ └── it should revert ✅ + └── when `_req.to` is not address(0) + ├── when `_req.quantity` is zero + │ └── it should revert ✅ + └── when `_req.quantity` is not zero + │ + │ // case: price is zero + └── when `_req.pricePerToken` is zero + │ └── when msg.value is not zero + │ │ └── it should revert ✅ + │ └── when msg.value is zero + │ ├── when `_req.to` address is an EOA + │ │ └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + │ │ └── it should increment `nextTokenIdToMint` by 1 ✅ + │ │ └── it should mint the `_req.quantity` number of tokens to the `_req.to` address ✅ + │ │ └── it should increment totalSupply of tokenId by `_req.quantity` ✅ + │ │ └── it should set `_req.uid` as minted ✅ + │ │ └── it should set uri for minted tokenId equal to `_req.uri` ✅ + │ │ └── it should emit MetadataUpdate event ✅ + │ │ └── it should emit TokensMintedWithSignature event ✅ + │ └── when `_to` address is a contract + │ ├── when `_to` address is non ERC1155Receiver implementer + │ │ └── it should revert ✅ + │ └── when `_to` address implements ERC1155Receiver + │ └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + │ └── it should increment `nextTokenIdToMint` by 1 ✅ + │ └── it should mint the `_req.quantity` number of tokens to the `_req.to` address ✅ + │ └── it should increment totalSupply of tokenId by `_req.quantity` ✅ + │ └── it should set `_req.uid` as minted ✅ + │ └── it should set uri for minted tokenId equal to `_uri` ✅ + │ └── it should emit MetadataUpdate event ✅ + │ └── it should emit TokensMintedWithSignature event ✅ + │ + │ // case: price is not zero + └── when `_req.pricePerToken` is not zero + └── when currency is native token + │ └── when msg.value is not equal to total price + │ │ └── it should revert ✅ + │ └── when msg.value is equal to total price + │ └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + │ └── it should increment `nextTokenIdToMint` by 1 ✅ + │ └── it should mint the `_req.quantity` number of tokens to the `_req.to` address ✅ + │ └── it should increment totalSupply of tokenId by `_req.quantity` ✅ + │ └── it should set `_req.uid` as minted ✅ + │ └── it should set uri for minted tokenId equal to `_uri` ✅ + │ └── (transfer to sale recipient) ✅ + │ └── (transfer to fee recipient) ✅ + │ └── it should emit MetadataUpdate event ✅ + │ └── it should emit TokensMintedWithSignature event ✅ + └── when currency is some ERC20 token + └── when msg.value is not zero + │ └── it should revert ✅ + └── when msg.value is zero + └── it should mint tokenId equal to current value of `nextTokenIdToMint` ✅ + └── it should increment `nextTokenIdToMint` by 1 ✅ + └── it should mint the `_req.quantity` number of tokens to the `_req.to` address ✅ + └── it should increment totalSupply of tokenId by `_req.quantity` ✅ + └── it should set `_req.uid` as minted ✅ + └── it should set uri for minted tokenId equal to `_uri` ✅ + └── (transfer to sale recipient) ✅ + └── (transfer to fee recipient) ✅ + └── it should emit MetadataUpdate event ✅ + └── it should emit TokensMintedWithSignature event ✅ + +// other cases + +├── when `_req.royaltyRecipient` is not address(0) + │ └── it should set royaltyInfoForToken ✅ + └── when `_req.royaltyRecipient` is address(0) + └── it should use default royalty info ✅ + +├── when reentrant call + └── it should revert ✅ + +├── when platformFeeType is flat + └── when total price is less than platform fee + │ └── it should revert ✅ + └── when total price is greater than or equal to platform fee + └── (transfer to sale recipient) ✅ + └── (transfer to fee recipient) ✅ + +├── when tokenId input is greater than or equal to nextTokenIdToMint + └── it should revert ✅ +├── when tokenId input is less than nextTokenIdToMint + └── it should mint ✅ + + diff --git a/src/test/tokenerc1155-BTT/other-functions/other.t.sol b/src/test/tokenerc1155-BTT/other-functions/other.t.sol new file mode 100644 index 000000000..fabdc6abf --- /dev/null +++ b/src/test/tokenerc1155-BTT/other-functions/other.t.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; +import { IStaking1155 } from "contracts/extension/interface/IStaking1155.sol"; +import { IERC2981 } from "contracts/eip/interface/IERC2981.sol"; + +import "@openzeppelin/contracts-upgradeable/access/IAccessControlEnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/IAccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 { + function canSetMetadata() public view returns (bool) { + return _canSetMetadata(); + } + + function canFreezeMetadata() public view returns (bool) { + return _canFreezeMetadata(); + } + + function beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) external { + _beforeTokenTransfer(operator, from, to, ids, amounts, data); + } + + function setTotalSupply(uint256 _tokenId, uint256 _totalSupply) external { + totalSupply[_tokenId] = _totalSupply; + } +} + +contract TokenERC1155Test_OtherFunctions is BaseTest { + address public implementation; + address public proxy; + + MyTokenERC1155 public tokenContract; + address internal caller; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + caller = getActor(3); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + } + + function test_contractType() public { + assertEq(tokenContract.contractType(), bytes32("TokenERC1155")); + } + + function test_contractVersion() public { + assertEq(tokenContract.contractVersion(), uint8(1)); + } + + function test_beforeTokenTransfer_restricted_notTransferRole() public { + uint256[] memory ids; + uint256[] memory amounts; + + vm.prank(deployer); + tokenContract.revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + vm.expectRevert("restricted to TRANSFER_ROLE holders."); + tokenContract.beforeTokenTransfer(caller, caller, address(0x123), ids, amounts, ""); + } + + modifier whenTransferRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("TRANSFER_ROLE"), caller); + _; + } + + function test_beforeTokenTransfer_restricted() public whenTransferRole { + uint256[] memory ids; + uint256[] memory amounts; + tokenContract.beforeTokenTransfer(caller, caller, address(0x123), ids, amounts, ""); + } + + function test_beforeTokenTransfer_restricted_fromZero() public whenTransferRole { + uint256[] memory ids = new uint256[](1); + uint256[] memory amounts = new uint256[](1); + uint256 _initialSupply = 100; + + ids[0] = 1; + amounts[0] = 10; + tokenContract.setTotalSupply(ids[0], _initialSupply); // mock set supply + + tokenContract.beforeTokenTransfer(caller, address(0), address(0x123), ids, amounts, ""); + + assertEq(tokenContract.totalSupply(ids[0]), amounts[0] + _initialSupply); + } + + function test_beforeTokenTransfer_restricted_toZero() public whenTransferRole { + uint256[] memory ids = new uint256[](1); + uint256[] memory amounts = new uint256[](1); + uint256 _initialSupply = 100; + + ids[0] = 1; + amounts[0] = 10; + tokenContract.setTotalSupply(ids[0], _initialSupply); // mock set supply + + tokenContract.beforeTokenTransfer(caller, caller, address(0), ids, amounts, ""); + + assertEq(tokenContract.totalSupply(ids[0]), _initialSupply - amounts[0]); + } + + function test_canSetMetadata_notMetadataRole() public { + assertFalse(tokenContract.canSetMetadata()); + } + + modifier whenMetadataRoleRole() { + _; + } + + function test_canSetMetadata() public whenMetadataRoleRole { + vm.prank(deployer); + assertTrue(tokenContract.canSetMetadata()); + } + + function test_canFreezeMetadata_notMetadataRole() public { + assertFalse(tokenContract.canFreezeMetadata()); + } + + function test_canFreezeMetadata() public whenMetadataRoleRole { + vm.prank(deployer); + assertTrue(tokenContract.canFreezeMetadata()); + } + + function test_supportsInterface() public { + assertTrue(tokenContract.supportsInterface(type(IERC2981).interfaceId)); + assertTrue(tokenContract.supportsInterface(type(IERC165).interfaceId)); + assertTrue(tokenContract.supportsInterface(type(IERC165Upgradeable).interfaceId)); + assertTrue(tokenContract.supportsInterface(type(IAccessControlEnumerableUpgradeable).interfaceId)); + assertTrue(tokenContract.supportsInterface(type(IAccessControlUpgradeable).interfaceId)); + assertTrue(tokenContract.supportsInterface(type(IERC1155Upgradeable).interfaceId)); + + // false for other not supported interfaces + assertFalse(tokenContract.supportsInterface(type(IStaking1155).interfaceId)); + } +} diff --git a/src/test/tokenerc1155-BTT/other-functions/other.tree b/src/test/tokenerc1155-BTT/other-functions/other.tree new file mode 100644 index 000000000..6af7d78cf --- /dev/null +++ b/src/test/tokenerc1155-BTT/other-functions/other.tree @@ -0,0 +1,37 @@ +contractType() +├── it should return bytes32("TokenERC1155") ✅ + +contractVersion() +├── it should return uint8(1) ✅ + +_beforeTokenTransfers( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data +) +├── when transfers are restricted (i.e. address(0) doesn't have transfer role, or from-to addresses are not address(0) + └── when from and to don't have transfer role + │ └── it should revert ✅ + └── when from is address(0) + │ └── it should increase totalSupply of `ids` by `amounts` ✅ + └── when to is address(0) + └── it should decrease totalSupply of `ids` by `amounts` ✅ + +_canSetMetadata() +├── when the caller doesn't have METADATA_ROLE +│ └── it should revert ✅ +└── when the caller has METADATA_ROLE + └── it should return true ✅ + +_canFreezeMetadata() +├── when the caller doesn't have METADATA_ROLE +│ └── it should revert ✅ +└── when the caller has METADATA_ROLE + └── it should return true ✅ + +supportsInterface(bytes4 interfaceId) +├── it should return true for supported interface ✅ +├── it should return false for not supported interface ✅ diff --git a/src/test/tokenerc1155-BTT/owner/owner.t.sol b/src/test/tokenerc1155-BTT/owner/owner.t.sol new file mode 100644 index 000000000..0615f32c4 --- /dev/null +++ b/src/test/tokenerc1155-BTT/owner/owner.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_Owner is BaseTest { + address public implementation; + address public proxy; + + MyTokenERC1155 internal tokenContract; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + } + + function test_owner() public { + assertEq(tokenContract.owner(), deployer); + } + + function test_owner_notDefaultAdmin() public { + vm.prank(deployer); + tokenContract.renounceRole(bytes32(0x00), deployer); + + assertEq(tokenContract.owner(), address(0)); + } +} diff --git a/src/test/tokenerc1155-BTT/owner/owner.tree b/src/test/tokenerc1155-BTT/owner/owner.tree new file mode 100644 index 000000000..576cfcb91 --- /dev/null +++ b/src/test/tokenerc1155-BTT/owner/owner.tree @@ -0,0 +1,6 @@ +owner() +├── when private variable `_owner` DEFAULT_ADMIN_ROLE +│ └── it should return `_owner` ✅ +└── when private variable `_owner` doesn't have DEFAULT_ADMIN_ROLE + └── it should return address(0) ✅ + diff --git a/src/test/tokenerc1155-BTT/set-contract-uri/setContractURI.t.sol b/src/test/tokenerc1155-BTT/set-contract-uri/setContractURI.t.sol new file mode 100644 index 000000000..494c6b5b1 --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-contract-uri/setContractURI.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_SetContractURI is BaseTest { + address public implementation; + address public proxy; + address internal caller; + string internal _contractURI; + + MyTokenERC1155 internal tokenContract; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + caller = getActor(1); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + _contractURI = "ipfs://contracturi"; + } + + function test_setContractURI_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + TWStrings.toHexString(uint160(caller), 20), + " is missing role ", + TWStrings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setContractURI(_contractURI); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setContractURI_empty() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setContractURI(""); + + // get contract uri + assertEq(tokenContract.contractURI(), ""); + } + + function test_setContractURI_notEmpty() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setContractURI(_contractURI); + + // get contract uri + assertEq(tokenContract.contractURI(), _contractURI); + } +} diff --git a/src/test/tokenerc1155-BTT/set-contract-uri/setContractURI.tree b/src/test/tokenerc1155-BTT/set-contract-uri/setContractURI.tree new file mode 100644 index 000000000..8fc480b19 --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-contract-uri/setContractURI.tree @@ -0,0 +1,8 @@ +setContractURI(string calldata _uri) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when `uri` is empty + │ └── it should update contract URI to empty string ✅ + └── when `uri` is not empty + └── it should update contract URI to `_uri` ✅ \ No newline at end of file diff --git a/src/test/tokenerc1155-BTT/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol b/src/test/tokenerc1155-BTT/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol new file mode 100644 index 000000000..e87e02948 --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_SetDefaultRoyaltyInfo is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal defaultRoyaltyRecipient; + uint256 internal defaultRoyaltyBps; + + MyTokenERC1155 internal tokenContract; + + event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + caller = getActor(1); + defaultRoyaltyRecipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + } + + function test_setDefaultRoyaltyInfo_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + TWStrings.toHexString(uint160(caller), 20), + " is missing role ", + TWStrings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setDefaultRoyaltyInfo_exceedMaxBps() public whenCallerAuthorized { + defaultRoyaltyBps = 10_001; + vm.prank(address(caller)); + vm.expectRevert("exceed royalty bps"); + tokenContract.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + modifier whenNotExceedMaxBps() { + defaultRoyaltyBps = 500; + _; + } + + function test_setDefaultRoyaltyInfo() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + tokenContract.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + + // get default royalty info + (address _recipient, uint16 _royaltyBps) = tokenContract.getDefaultRoyaltyInfo(); + assertEq(_recipient, defaultRoyaltyRecipient); + assertEq(_royaltyBps, uint16(defaultRoyaltyBps)); + + // get royalty info for token + uint256 tokenId = 0; + (_recipient, _royaltyBps) = tokenContract.getRoyaltyInfoForToken(tokenId); + assertEq(_recipient, defaultRoyaltyRecipient); + assertEq(_royaltyBps, uint16(defaultRoyaltyBps)); + + // royaltyInfo - ERC2981 + uint256 salePrice = 1000; + (address _royaltyRecipient, uint256 _royaltyAmount) = tokenContract.royaltyInfo(tokenId, salePrice); + assertEq(_royaltyRecipient, defaultRoyaltyRecipient); + assertEq(_royaltyAmount, (salePrice * defaultRoyaltyBps) / 10_000); + } + + function test_setDefaultRoyaltyInfo_event() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + vm.expectEmit(true, false, false, true); + emit DefaultRoyalty(defaultRoyaltyRecipient, defaultRoyaltyBps); + tokenContract.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } +} diff --git a/src/test/tokenerc1155-BTT/set-default-royalty-info/setDefaultRoyaltyInfo.tree b/src/test/tokenerc1155-BTT/set-default-royalty-info/setDefaultRoyaltyInfo.tree new file mode 100644 index 000000000..78a4312de --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-default-royalty-info/setDefaultRoyaltyInfo.tree @@ -0,0 +1,11 @@ +setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when royalty bps input is greater than MAX_BPS + │ └── it should revert ✅ + └── when royalty bps input is less than or equal to MAX_BPS + └── it should update default royalty recipient ✅ + └── it should update default royalty bps ✅ + └── it should calculate royalty amount for a token-id based on default royalty info ✅ + └── it should emit DefaultRoyalty event ✅ \ No newline at end of file diff --git a/src/test/tokenerc1155-BTT/set-flat-platform-fee-info/setFlatPlatformFeeInfo.t.sol b/src/test/tokenerc1155-BTT/set-flat-platform-fee-info/setFlatPlatformFeeInfo.t.sol new file mode 100644 index 000000000..798cbd285 --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-flat-platform-fee-info/setFlatPlatformFeeInfo.t.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_SetFlatPlatformFeeInfo is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal _platformFeeRecipient; + uint256 internal _flatFee; + + MyTokenERC1155 internal tokenContract; + + event FlatPlatformFeeUpdated(address platformFeeRecipient, uint256 flatFee); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + caller = getActor(1); + _platformFeeRecipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + _flatFee = 25; + } + + function test_setFlatPlatformFeeInfo_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + TWStrings.toHexString(uint160(caller), 20), + " is missing role ", + TWStrings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setFlatPlatformFeeInfo(_platformFeeRecipient, _flatFee); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setFlatPlatformFeeInfo() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setFlatPlatformFeeInfo(_platformFeeRecipient, _flatFee); + + // get platform fee info + (address _recipient, uint256 _fee) = tokenContract.getFlatPlatformFeeInfo(); + assertEq(_recipient, _platformFeeRecipient); + assertEq(_fee, _flatFee); + assertEq(tokenContract.platformFeeRecipient(), _platformFeeRecipient); + } + + function test_setFlatPlatformFeeInfo_event() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectEmit(false, false, false, true); + emit FlatPlatformFeeUpdated(_platformFeeRecipient, _flatFee); + tokenContract.setFlatPlatformFeeInfo(_platformFeeRecipient, _flatFee); + } +} diff --git a/src/test/tokenerc1155-BTT/set-flat-platform-fee-info/setFlatPlatformFeeInfo.tree b/src/test/tokenerc1155-BTT/set-flat-platform-fee-info/setFlatPlatformFeeInfo.tree new file mode 100644 index 000000000..95bfe1f2d --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-flat-platform-fee-info/setFlatPlatformFeeInfo.tree @@ -0,0 +1,8 @@ +setFlatPlatformFeeInfo(address _platformFeeRecipient, uint256 _flatFee) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when `_platformFeeBps` is less than or equal to MAX_BPS + └── it should update platform fee recipient ✅ + └── it should update flatPlatformFee ✅ + └── it should emit FlatPlatformFeeUpdated event ✅ \ No newline at end of file diff --git a/src/test/tokenerc1155-BTT/set-owner/setOwner.t.sol b/src/test/tokenerc1155-BTT/set-owner/setOwner.t.sol new file mode 100644 index 000000000..7f8145f07 --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-owner/setOwner.t.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_SetOwner is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal _newOwner; + + MyTokenERC1155 internal tokenContract; + + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + caller = getActor(1); + _newOwner = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + } + + function test_setOwner_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + TWStrings.toHexString(uint160(caller), 20), + " is missing role ", + TWStrings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setOwner(_newOwner); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setOwner_newOwnerNotAdmin() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectRevert("new owner not module admin."); + tokenContract.setOwner(_newOwner); + } + + modifier whenNewOwnerIsAnAdmin() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), _newOwner); + _; + } + + function test_setOwner() public whenCallerAuthorized whenNewOwnerIsAnAdmin { + vm.prank(address(caller)); + tokenContract.setOwner(_newOwner); + + assertEq(tokenContract.owner(), _newOwner); + } + + function test_setOwner_event() public whenCallerAuthorized whenNewOwnerIsAnAdmin { + vm.prank(address(caller)); + vm.expectEmit(true, true, false, false); + emit OwnerUpdated(deployer, _newOwner); + tokenContract.setOwner(_newOwner); + } +} diff --git a/src/test/tokenerc1155-BTT/set-owner/setOwner.tree b/src/test/tokenerc1155-BTT/set-owner/setOwner.tree new file mode 100644 index 000000000..964e97cac --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-owner/setOwner.tree @@ -0,0 +1,9 @@ +setOwner(address _newOwner) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when incoming `_owner` doesn't have DEFAULT_ADMIN_ROLE + │ └── it should revert ✅ + └── when incoming `_owner` has DEFAULT_ADMIN_ROLE + └── it should update owner ✅ + └── it should emit OwnerUpdated event ✅ \ No newline at end of file diff --git a/src/test/tokenerc1155-BTT/set-platform-fee-info/setPlatformFeeInfo.t.sol b/src/test/tokenerc1155-BTT/set-platform-fee-info/setPlatformFeeInfo.t.sol new file mode 100644 index 000000000..edeb78fe6 --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-platform-fee-info/setPlatformFeeInfo.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_SetPlatformFeeInfo is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal _platformFeeRecipient; + uint256 internal _platformFeeBps; + + MyTokenERC1155 internal tokenContract; + + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + caller = getActor(1); + _platformFeeRecipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + } + + function test_setPlatformFeeInfo_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + TWStrings.toHexString(uint160(caller), 20), + " is missing role ", + TWStrings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setPlatformFeeInfo_exceedMaxBps() public whenCallerAuthorized { + _platformFeeBps = 10_001; + vm.prank(address(caller)); + vm.expectRevert("exceeds MAX_BPS"); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + modifier whenNotExceedMaxBps() { + _platformFeeBps = 500; + _; + } + + function test_setPlatformFeeInfo() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + + // get platform fee info + (address _recipient, uint16 _bps) = tokenContract.getPlatformFeeInfo(); + assertEq(_recipient, _platformFeeRecipient); + assertEq(_bps, uint16(_platformFeeBps)); + assertEq(tokenContract.platformFeeRecipient(), _platformFeeRecipient); + } + + function test_setPlatformFeeInfo_event() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + vm.expectEmit(true, false, false, true); + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } +} diff --git a/src/test/tokenerc1155-BTT/set-platform-fee-info/setPlatformFeeInfo.tree b/src/test/tokenerc1155-BTT/set-platform-fee-info/setPlatformFeeInfo.tree new file mode 100644 index 000000000..dcef9965e --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-platform-fee-info/setPlatformFeeInfo.tree @@ -0,0 +1,10 @@ +setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when `_platformFeeBps` is greater than MAX_BPS + │ └── it should revert ✅ + └── when `_platformFeeBps` is less than or equal to MAX_BPS + └── it should update platform fee recipient ✅ + └── it should update platform fee bps ✅ + └── it should emit PlatformFeeInfoUpdated event ✅ \ No newline at end of file diff --git a/src/test/tokenerc1155-BTT/set-platform-fee-type/setPlatformFeeType.t.sol b/src/test/tokenerc1155-BTT/set-platform-fee-type/setPlatformFeeType.t.sol new file mode 100644 index 000000000..a8f85ceb4 --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-platform-fee-type/setPlatformFeeType.t.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; +import { IPlatformFee } from "contracts/extension/interface/IPlatformFee.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_SetPlatformFeeType is BaseTest { + address public implementation; + address public proxy; + address internal caller; + IPlatformFee.PlatformFeeType internal _newFeeType; + + MyTokenERC1155 internal tokenContract; + + event PlatformFeeTypeUpdated(IPlatformFee.PlatformFeeType feeType); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + caller = getActor(1); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + _newFeeType = IPlatformFee.PlatformFeeType.Flat; + } + + function test_setPlatformFeeType_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + TWStrings.toHexString(uint160(caller), 20), + " is missing role ", + TWStrings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setPlatformFeeType(_newFeeType); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setPlatformFeeType() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setPlatformFeeType(_newFeeType); + + assertEq(uint8(tokenContract.getPlatformFeeType()), uint8(_newFeeType)); + } + + function test_setPlatformFeeType_event() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectEmit(false, false, false, true); + emit PlatformFeeTypeUpdated(_newFeeType); + tokenContract.setPlatformFeeType(_newFeeType); + } +} diff --git a/src/test/tokenerc1155-BTT/set-platform-fee-type/setPlatformFeeType.tree b/src/test/tokenerc1155-BTT/set-platform-fee-type/setPlatformFeeType.tree new file mode 100644 index 000000000..e25a6bd4c --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-platform-fee-type/setPlatformFeeType.tree @@ -0,0 +1,6 @@ +setPlatformFeeType(PlatformFeeType _feeType) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── it should update platformFeeType ✅ + └── it should emit PlatformFeeTypeUpdated event ✅ \ No newline at end of file diff --git a/src/test/tokenerc1155-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.t.sol b/src/test/tokenerc1155-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.t.sol new file mode 100644 index 000000000..a8f6b7a1d --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.t.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_SetPrimarySaleRecipient is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal _primarySaleRecipient; + + MyTokenERC1155 internal tokenContract; + + event PrimarySaleRecipientUpdated(address indexed recipient); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + caller = getActor(1); + _primarySaleRecipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + } + + function test_setPrimarySaleRecipient_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + TWStrings.toHexString(uint160(caller), 20), + " is missing role ", + TWStrings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setPrimarySaleRecipient() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + + // get primary sale recipient info + assertEq(tokenContract.primarySaleRecipient(), _primarySaleRecipient); + } + + function test_setPrimarySaleRecipient_event() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectEmit(true, false, false, false); + emit PrimarySaleRecipientUpdated(_primarySaleRecipient); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + } +} diff --git a/src/test/tokenerc1155-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.tree b/src/test/tokenerc1155-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.tree new file mode 100644 index 000000000..230035a07 --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.tree @@ -0,0 +1,6 @@ +setPrimarySaleRecipient(address _saleRecipient) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── it should update primary sale recipient ✅ + └── it should emit PrimarySaleRecipientUpdated event ✅ \ No newline at end of file diff --git a/src/test/tokenerc1155-BTT/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol b/src/test/tokenerc1155-BTT/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol new file mode 100644 index 000000000..a0bd34870 --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_SetRoyaltyInfoForToken is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal defaultRoyaltyRecipient; + uint256 internal defaultRoyaltyBps; + + MyTokenERC1155 internal tokenContract; + + address internal royaltyRecipientForToken; + uint256 internal royaltyBpsForToken; + uint256 internal tokenId; + + event RoyaltyForToken(uint256 indexed tokenId, address indexed royaltyRecipient, uint256 royaltyBps); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + caller = getActor(1); + defaultRoyaltyRecipient = getActor(2); + royaltyRecipientForToken = getActor(3); + defaultRoyaltyBps = 500; + tokenId = 1; + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + + vm.prank(deployer); + tokenContract.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + function test_setRoyaltyInfoForToken_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + TWStrings.toHexString(uint160(caller), 20), + " is missing role ", + TWStrings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setRoyaltyInfoForToken_exceedMaxBps() public whenCallerAuthorized { + royaltyBpsForToken = 10_001; + vm.prank(address(caller)); + vm.expectRevert("exceed royalty bps"); + tokenContract.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } + + modifier whenNotExceedMaxBps() { + royaltyBpsForToken = 1000; + _; + } + + function test_setRoyaltyInfoForToken() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + tokenContract.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + + // get default royalty info + (address _defaultRecipient, uint16 _defaultRoyaltyBps) = tokenContract.getDefaultRoyaltyInfo(); + assertEq(_defaultRecipient, defaultRoyaltyRecipient); + assertEq(_defaultRoyaltyBps, uint16(defaultRoyaltyBps)); + + // get royalty info for token + (address _royaltyRecipientForToken, uint16 _royaltyBpsForToken) = tokenContract.getRoyaltyInfoForToken(tokenId); + assertEq(_royaltyRecipientForToken, royaltyRecipientForToken); + assertEq(_royaltyBpsForToken, uint16(royaltyBpsForToken)); + + // royaltyInfo - ERC2981: calculate for default + uint256 salePrice = 1000; + (address _royaltyRecipient, uint256 _royaltyAmount) = tokenContract.royaltyInfo(0, salePrice); + assertEq(_royaltyRecipient, defaultRoyaltyRecipient); + assertEq(_royaltyAmount, (salePrice * defaultRoyaltyBps) / 10_000); + + // royaltyInfo - ERC2981: calculate for specific tokenId we set the royalty info for + (_royaltyRecipient, _royaltyAmount) = tokenContract.royaltyInfo(tokenId, salePrice); + assertEq(_royaltyRecipient, royaltyRecipientForToken); + assertEq(_royaltyAmount, (salePrice * royaltyBpsForToken) / 10_000); + } + + function test_setRoyaltyInfoForToken_event() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + vm.expectEmit(true, true, false, true); + emit RoyaltyForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + tokenContract.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } +} diff --git a/src/test/tokenerc1155-BTT/set-royalty-info-for-token/setRoyaltyInfoForToken.tree b/src/test/tokenerc1155-BTT/set-royalty-info-for-token/setRoyaltyInfoForToken.tree new file mode 100644 index 000000000..cada076de --- /dev/null +++ b/src/test/tokenerc1155-BTT/set-royalty-info-for-token/setRoyaltyInfoForToken.tree @@ -0,0 +1,15 @@ +function setRoyaltyInfoForToken( + uint256 _tokenId, + address _recipient, + uint256 _bps +) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when royalty bps input is greater than MAX_BPS + │ └── it should revert ✅ + └── when royalty bps input is less than or equal to MAX_BPS + └── it should update default royalty recipient ✅ + └── it should update default royalty bps ✅ + └── it should calculate royalty amount for a token-id based on default royalty info ✅ + └── it should emit RoyaltyForToken event ✅ \ No newline at end of file diff --git a/src/test/tokenerc1155-BTT/uri/tokenURI.t.sol b/src/test/tokenerc1155-BTT/uri/tokenURI.t.sol new file mode 100644 index 000000000..1a2feb0a2 --- /dev/null +++ b/src/test/tokenerc1155-BTT/uri/tokenURI.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 {} + +contract TokenERC1155Test_Uri is BaseTest { + address public implementation; + address public proxy; + + MyTokenERC1155 internal tokenContract; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + } + + function test_uri() public { + uint256 _tokenId = 1; + string memory _uri = "ipfs://uri/1"; + + vm.prank(deployer); + tokenContract.setTokenURI(_tokenId, _uri); + + assertEq(tokenContract.uri(_tokenId), _uri); + } +} diff --git a/src/test/tokenerc1155-BTT/uri/tokenURI.tree b/src/test/tokenerc1155-BTT/uri/tokenURI.tree new file mode 100644 index 000000000..2df0b55ed --- /dev/null +++ b/src/test/tokenerc1155-BTT/uri/tokenURI.tree @@ -0,0 +1,3 @@ +uri(uint256 _tokenId) +├── it should return uri associated with the given `_tokenId` ✅ + diff --git a/src/test/tokenerc1155-BTT/verify/verify.t.sol b/src/test/tokenerc1155-BTT/verify/verify.t.sol new file mode 100644 index 000000000..7b647cd10 --- /dev/null +++ b/src/test/tokenerc1155-BTT/verify/verify.t.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC1155 is TokenERC1155 { + function setMintedURI(MintRequest calldata _req, bytes calldata _signature) external { + verifyRequest(_req, _signature); + } +} + +contract TokenERC1155Test_Verify is BaseTest { + address public implementation; + address public proxy; + + MyTokenERC1155 internal tokenContract; + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + TokenERC1155.MintRequest _mintrequest; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC1155()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC1155.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + + tokenContract = MyTokenERC1155(proxy); + + typehashMintRequest = keccak256( + "MintRequest(address to,address royaltyRecipient,uint256 royaltyBps,address primarySaleRecipient,uint256 tokenId,string uri,uint256 quantity,uint256 pricePerToken,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes("TokenERC1155")); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tokenContract)) + ); + + // construct default mintrequest + _mintrequest.to = address(0x1234); + _mintrequest.royaltyRecipient = royaltyRecipient; + _mintrequest.royaltyBps = royaltyBps; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.tokenId = type(uint256).max; + _mintrequest.uri = "ipfs://"; + _mintrequest.quantity = 100; + _mintrequest.pricePerToken = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 0; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + } + + function signMintRequest(TokenERC1155.MintRequest memory _request, uint256 _privateKey) + internal + view + returns (bytes memory) + { + bytes memory encodedRequest = bytes.concat( + abi.encode( + typehashMintRequest, + _request.to, + _request.royaltyRecipient, + _request.royaltyBps, + _request.primarySaleRecipient, + _request.tokenId, + keccak256(bytes(_request.uri)) + ), + abi.encode( + _request.quantity, + _request.pricePerToken, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ) + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + function test_verify_notMinterRole() public { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + (bool _isValid, address _recoveredSigner) = tokenContract.verify(_mintrequest, _signature); + + assertFalse(_isValid); + assertEq(_recoveredSigner, signer); + } + + modifier whenMinterRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), signer); + _; + } + + function test_verify_invalidUID() public whenMinterRole { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // set state with this mintrequest and signature, marking the UID as used + tokenContract.setMintedURI(_mintrequest, _signature); + + // pass the same UID mintrequest again + (bool _isValid, address _recoveredSigner) = tokenContract.verify(_mintrequest, _signature); + + assertFalse(_isValid); + assertEq(_recoveredSigner, signer); + } + + modifier whenUidNotUsed() { + _; + } + + function test_verify() public whenMinterRole whenUidNotUsed { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + (bool _isValid, address _recoveredSigner) = tokenContract.verify(_mintrequest, _signature); + + assertTrue(_isValid); + assertEq(_recoveredSigner, signer); + } +} diff --git a/src/test/tokenerc1155-BTT/verify/verify.tree b/src/test/tokenerc1155-BTT/verify/verify.tree new file mode 100644 index 000000000..c160faa0f --- /dev/null +++ b/src/test/tokenerc1155-BTT/verify/verify.tree @@ -0,0 +1,12 @@ +verify(MintRequest calldata _req, bytes calldata _signature) +├── when signer doesn't have MINTER_ROLE +│ └── it should return false ✅ +│ └── it should return recovered signer equal to the actual signer of the request ✅ +└── when signer has MINTER_ROLE + └── when `_req.uid` has already been used + │ └── it should return false ✅ + │ └── it should return recovered signer equal to the actual signer of the request ✅ + └── when `_req.uid` has not been used + └── it should return true ✅ + └── it should return recovered signer equal to the actual signer of the request ✅ +