diff --git a/contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol b/contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol new file mode 100644 index 000000000..4ad26f927 --- /dev/null +++ b/contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +/// @author thirdweb + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +// ========== External imports ========== + +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; + +import "../../eip/queryable/ERC721AQueryableUpgradeable.sol"; + +// ========== Internal imports ========== + +import "../../external-deps/openzeppelin/metatx/ERC2771ContextUpgradeable.sol"; +import "../../lib/CurrencyTransferLib.sol"; + +// ========== Features ========== + +import "../../extension/Multicall.sol"; +import "../../extension/ContractMetadata.sol"; +import "../../extension/Royalty.sol"; +import "../../extension/PrimarySale.sol"; +import "../../extension/Ownable.sol"; +import "../../extension/SharedMetadata.sol"; +import "../../extension/PermissionsEnumerable.sol"; +import "../../extension/Drop.sol"; +import "../../extension/PlatformFee.sol"; + +contract OpenEditionERC721FlatFee is + Initializable, + ContractMetadata, + PlatformFee, + Royalty, + PrimarySale, + Ownable, + SharedMetadata, + PermissionsEnumerable, + Drop, + ERC2771ContextUpgradeable, + Multicall, + ERC721AQueryableUpgradeable +{ + using StringsUpgradeable for uint256; + + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. + bytes32 private transferRole; + /// @dev Only MINTER_ROLE holders can update the shared metadata of tokens. + bytes32 private minterRole; + + /// @dev Max bps in the thirdweb system. + uint256 private constant MAX_BPS = 10_000; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor() initializer {} + + /// @dev Initializes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient + ) external initializerERC721A initializer { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + bytes32 _minterRole = keccak256("MINTER_ROLE"); + + // Initialize inherited contracts, most base-like -> most derived. + __ERC2771Context_init(_trustedForwarders); + __ERC721A_init(_name, _symbol); + + _setupContractURI(_contractURI); + _setupOwner(_defaultAdmin); + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(_minterRole, _defaultAdmin); + _setupRole(_transferRole, _defaultAdmin); + _setupRole(_transferRole, address(0)); + + _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + _setupPrimarySaleRecipient(_saleRecipient); + + transferRole = _transferRole; + minterRole = _minterRole; + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 721 / 2981 logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the URI for a given tokenId. + function tokenURI( + uint256 _tokenId + ) public view virtual override(ERC721AUpgradeable, IERC721AUpgradeable) returns (string memory) { + if (!_exists(_tokenId)) { + revert("!ID"); + } + + return _getURIFromSharedMetadata(_tokenId); + } + + /// @dev See ERC 165 + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC721AUpgradeable, IERC165, IERC721AUpgradeable) returns (bool) { + return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; + } + + /// @dev The start token ID for the contract. + function _startTokenId() internal pure override returns (uint256) { + return 1; + } + + function startTokenId() public pure returns (uint256) { + return _startTokenId(); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Collects and distributes the primary sale value of NFTs being claimed. + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override { + if (_pricePerToken == 0) { + require(msg.value == 0, "!Value"); + return; + } + + uint256 totalPrice = _quantityToClaim * _pricePerToken; + uint256 platformFees; + address platformFeeRecipient; + + if (getPlatformFeeType() == IPlatformFee.PlatformFeeType.Flat) { + (platformFeeRecipient, platformFees) = getFlatPlatformFeeInfo(); + } else { + (address recipient, uint16 platformFeeBps) = getPlatformFeeInfo(); + platformFeeRecipient = recipient; + platformFees = ((totalPrice * platformFeeBps) / MAX_BPS); + } + require(totalPrice >= platformFees, "price less than platform fee"); + + bool validMsgValue; + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { + validMsgValue = msg.value == totalPrice; + } else { + validMsgValue = msg.value == 0; + } + require(validMsgValue, "!V"); + + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; + + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), platformFeeRecipient, platformFees); + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), saleRecipient, totalPrice - platformFees); + } + + /// @dev Transfers the NFTs being claimed. + function _transferTokensOnClaim( + address _to, + uint256 _quantityBeingClaimed + ) internal override returns (uint256 startTokenId_) { + startTokenId_ = _nextTokenId(); + _safeMint(_to, _quantityBeingClaimed); + } + + /// @dev Checks whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether owner can be set in the given execution context. + function _canSetOwner() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetClaimConditions() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /// @dev Returns whether the shared metadata of tokens can be set in the given execution context. + function _canSetSharedMetadata() internal view virtual override returns (bool) { + return hasRole(minterRole, _msgSender()); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function _canSetPlatformFeeInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + /** + * Returns the total amount of tokens minted in the contract. + */ + function totalMinted() external view returns (uint256) { + unchecked { + return _nextTokenId() - _startTokenId(); + } + } + + /// @dev The tokenId of the next NFT that will be minted / lazy minted. + function nextTokenIdToMint() external view returns (uint256) { + return _nextTokenId(); + } + + /// @dev The next token ID of the NFT that can be claimed. + function nextTokenIdToClaim() external view returns (uint256) { + return _nextTokenId(); + } + + /// @dev Burns `tokenId`. See {ERC721-_burn}. + function burn(uint256 tokenId) external virtual { + // note: ERC721AUpgradeable's `_burn(uint256,bool)` internally checks for token approvals. + _burn(tokenId, true); + } + + /// @dev See {ERC721-_beforeTokenTransfer}. + function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId_, + uint256 quantity + ) internal virtual override { + super._beforeTokenTransfers(from, to, startTokenId_, quantity); + + // if transfer is restricted on the contract, we still want to allow burning and minting + if (!hasRole(transferRole, address(0)) && from != address(0) && to != address(0)) { + if (!hasRole(transferRole, from) && !hasRole(transferRole, to)) { + revert("!T"); + } + } + } + + function _dropMsgSender() internal view virtual override returns (address) { + return _msgSender(); + } + + function _msgSenderERC721A() internal view virtual override returns (address) { + return _msgSender(); + } + + function _msgSender() + internal + view + virtual + override(ERC2771ContextUpgradeable, Multicall) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } +} diff --git a/src/test/OpenEditionERC721FlatFee.t.sol b/src/test/OpenEditionERC721FlatFee.t.sol new file mode 100644 index 000000000..4c94b0ae7 --- /dev/null +++ b/src/test/OpenEditionERC721FlatFee.t.sol @@ -0,0 +1,796 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { ERC721AUpgradeable, OpenEditionERC721FlatFee, ISharedMetadata } from "contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol"; +import { NFTMetadataRenderer } from "contracts/lib/NFTMetadataRenderer.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; +import "./utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; + +contract OpenEditionERC721FlatFeeTest is BaseTest { + using Strings for uint256; + using Strings for address; + + event SharedMetadataUpdated(string name, string description, string imageURI, string animationURI); + + OpenEditionERC721FlatFee public openEdition; + ISharedMetadata.SharedMetadataInfo public sharedMetadata; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + address openEditionImpl = address(new OpenEditionERC721FlatFee()); + + vm.prank(deployer); + openEdition = OpenEditionERC721FlatFee( + address( + new TWProxy( + openEditionImpl, + abi.encodeCall( + OpenEditionERC721FlatFee.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ); + + sharedMetadata = ISharedMetadata.SharedMetadataInfo({ + name: "Test", + description: "Test", + imageURI: "https://test.com", + animationURI: "https://test.com" + }); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc. + //////////////////////////////////////////////////////////////*/ + + /** + * note: Tests whether contract reverts when a non-holder renounces a role. + */ + function test_revert_nonHolder_renounceRole() public { + address caller = address(0x123); + bytes32 role = keccak256("MINTER_ROLE"); + + vm.prank(caller); + vm.expectRevert( + abi.encodePacked( + "Permissions: account ", + Strings.toHexString(uint160(caller), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + + openEdition.renounceRole(role, caller); + } + + /** + * note: Tests whether contract reverts when a role admin revokes a role for a non-holder. + */ + function test_revert_revokeRoleForNonHolder() public { + address target = address(0x123); + bytes32 role = keccak256("MINTER_ROLE"); + + vm.prank(deployer); + vm.expectRevert( + abi.encodePacked( + "Permissions: account ", + Strings.toHexString(uint160(target), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ); + + openEdition.revokeRole(role, target); + } + + /** + * @dev Tests whether contract reverts when a role is granted to an existent role holder. + */ + function test_revert_grant_role_to_account_with_role() public { + bytes32 role = keccak256("ABC_ROLE"); + address receiver = getActor(0); + + vm.startPrank(deployer); + + openEdition.grantRole(role, receiver); + + vm.expectRevert("Can only grant to non holders"); + openEdition.grantRole(role, receiver); + + vm.stopPrank(); + } + + /** + * @dev Tests contract state for Transfer role. + */ + function test_state_grant_transferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + + // check if admin and address(0) have transfer role in the beginning + bool checkAddressZero = openEdition.hasRole(role, address(0)); + bool checkAdmin = openEdition.hasRole(role, deployer); + assertTrue(checkAddressZero); + assertTrue(checkAdmin); + + // check if transfer role can be granted to a non-holder + address receiver = getActor(0); + vm.startPrank(deployer); + openEdition.grantRole(role, receiver); + + // expect revert when granting to a holder + vm.expectRevert("Can only grant to non holders"); + openEdition.grantRole(role, receiver); + + // check if receiver has transfer role + bool checkReceiver = openEdition.hasRole(role, receiver); + assertTrue(checkReceiver); + + // check if role is correctly revoked + openEdition.revokeRole(role, receiver); + checkReceiver = openEdition.hasRole(role, receiver); + assertFalse(checkReceiver); + openEdition.revokeRole(role, address(0)); + checkAddressZero = openEdition.hasRole(role, address(0)); + assertFalse(checkAddressZero); + + vm.stopPrank(); + } + + /** + * note: Testing transfer of tokens when transfer-role is restricted + */ + function test_claim_transferRole() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + openEdition.claim(receiver, 1, address(0), 0, alp, ""); + + // revoke transfer role from address(0) + vm.prank(deployer); + openEdition.revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + vm.startPrank(receiver); + vm.expectRevert(bytes("!T")); + openEdition.transferFrom(receiver, address(123), 1); + } + + function test_claimCondition_with_startTimestamp() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].startTimestamp = 100; + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.warp(99); + vm.prank(getActor(5), getActor(5)); + vm.expectRevert("!CONDITION."); + openEdition.claim(receiver, 1, address(0), 0, alp, ""); + + vm.warp(100); + vm.prank(getActor(4), getActor(4)); + openEdition.claim(receiver, 1, address(0), 0, alp, ""); + } + + /*/////////////////////////////////////////////////////////////// + Set Shared Metadata Tests + //////////////////////////////////////////////////////////////*/ + + /* + * note: Testing state changes; set shared metadata for tokens. + */ + function test_state_sharedMetadata() public { + // SET METADATA + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + + // CLAIM 1 TOKEN + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + openEdition.claim(receiver, 100, address(erc20), 0, alp, ""); + + string memory uri = openEdition.tokenURI(1); + assertEq( + uri, + NFTMetadataRenderer.createMetadataEdition({ + name: sharedMetadata.name, + description: sharedMetadata.description, + imageURI: sharedMetadata.imageURI, + animationURI: sharedMetadata.animationURI, + tokenOfEdition: 1 + }) + ); + } + + /** + * note: Testing revert condition; an address without MINTER_ROLE calls setSharedMetadata function. + */ + function test_revert_setSharedMetadata_MINTER_ROLE() public { + vm.expectRevert(); + openEdition.setSharedMetadata(sharedMetadata); + } + + /** + * note: Testing event emission; shared metadata set. + */ + function test_event_setSharedMetadata_SharedMetadataUpdated() public { + vm.startPrank(deployer); + + vm.expectEmit(true, false, false, true); + emit SharedMetadataUpdated( + sharedMetadata.name, + sharedMetadata.description, + sharedMetadata.imageURI, + sharedMetadata.animationURI + ); + openEdition.setSharedMetadata(sharedMetadata); + + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Claim Tests + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing revert condition; exceed max claimable supply. + */ + function test_revert_claimCondition_exceedMaxClaimableSupply() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 200; + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + openEdition.claim(receiver, 100, address(0), 0, alp, ""); + + vm.expectRevert("!MaxSupply"); + vm.prank(getActor(6), getActor(6)); + openEdition.claim(receiver, 1, address(0), 0, alp, ""); + } + + /** + * note: Testing quantity limit restriction when no allowlist present. + */ + function test_fuzz_claim_noAllowlist(uint256 x) public { + vm.assume(x != 0); + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + bytes memory errorQty = "!Qty"; + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + openEdition.claim(receiver, 0, address(0), 0, alp, ""); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + openEdition.claim(receiver, 101, address(0), 0, alp, ""); + + vm.prank(deployer); + openEdition.setClaimConditions(conditions, true); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + openEdition.claim(receiver, 101, address(0), 0, alp, ""); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price set to 0 + */ + function test_state_claim_allowlisted_SetQuantityZeroPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + openEdition.claim(receiver, 100, address(erc20), 0, alp, ""); // claims for free, because allowlist price is 0 + assertEq(openEdition.getSupplyClaimedByWallet(openEdition.getActiveClaimConditionId(), receiver), 100); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price set to non-zero value + */ + function test_state_claim_allowlisted_SetQuantityPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "5"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 5; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.prank(receiver, receiver); + vm.expectRevert("!PriceOrCurrency"); + openEdition.claim(receiver, 100, address(erc20), 0, alp, ""); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(openEdition), 10000); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + openEdition.claim(receiver, 100, address(erc20), 5, alp, ""); + assertEq(openEdition.getSupplyClaimedByWallet(openEdition.getActiveClaimConditionId(), receiver), 100); + assertEq(erc20.balanceOf(receiver), 10000 - 500); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to some value different than general limit + * - allowlist price not set; should default to general price and currency + */ + function test_state_claim_allowlisted_SetQuantityDefaultPrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = Strings.toString(type(uint256).max); // this implies that general price is applicable + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = type(uint256).max; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(openEdition), 10000); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + openEdition.claim(receiver, 100, address(erc20), 10, alp, ""); + assertEq(openEdition.getSupplyClaimedByWallet(openEdition.getActiveClaimConditionId(), receiver), 100); + assertEq(erc20.balanceOf(receiver), 10000 - 1000); + } + + /** + * note: Testing quantity limit restriction + * - allowlist quantity set to 0 => should default to general limit + * - allowlist price set to some value different than general price + */ + function test_state_claim_allowlisted_DefaultQuantitySomePrice() public { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "0"; // this implies that general limit is applicable + inputs[3] = "5"; + inputs[4] = "0x0000000000000000000000000000000000000000"; // general currency will be applicable + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 0; + alp.pricePerToken = 5; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); // in allowlist + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(openEdition), 10000); + + bytes memory errorQty = "!Qty"; + vm.prank(receiver, receiver); + vm.expectRevert(errorQty); + openEdition.claim(receiver, 100, address(erc20), 5, alp, ""); // trying to claim more than general limit + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + openEdition.claim(receiver, 10, address(erc20), 5, alp, ""); + assertEq(openEdition.getSupplyClaimedByWallet(openEdition.getActiveClaimConditionId(), receiver), 10); + assertEq(erc20.balanceOf(receiver), 10000 - 50); + } + + function test_fuzz_claim_merkleProof(uint256 x) public { + vm.assume(x > 10 && x < 500); + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = Strings.toString(x); + inputs[3] = "0"; + inputs[4] = "0x0000000000000000000000000000000000000000"; + + bytes memory result = vm.ffi(inputs); + // revert(); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + alp.pricePerToken = 0; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd); + + // bytes32[] memory proofs = new bytes32[](0); + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = x; + conditions[0].quantityLimitPerWallet = 1; + conditions[0].merkleRoot = root; + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + openEdition.claim(receiver, x - 5, address(0), 0, alp, ""); + assertEq(openEdition.getSupplyClaimedByWallet(openEdition.getActiveClaimConditionId(), receiver), x - 5); + + bytes memory errorQty = "!Qty"; + + vm.prank(receiver, receiver); + vm.expectRevert(errorQty); + openEdition.claim(receiver, 6, address(0), 0, alp, ""); + + vm.prank(receiver, receiver); + openEdition.claim(receiver, 5, address(0), 0, alp, ""); + assertEq(openEdition.getSupplyClaimedByWallet(openEdition.getActiveClaimConditionId(), receiver), x); + + vm.prank(receiver, receiver); + vm.expectRevert(errorQty); + openEdition.claim(receiver, 5, address(0), 0, alp, ""); // quantity limit already claimed + } + + /** + * note: Testing state changes; reset eligibility of claim conditions and claiming again for same condition id. + */ + function test_state_claimCondition_resetEligibility() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + openEdition.claim(receiver, 100, address(0), 0, alp, ""); + + bytes memory errorQty = "!Qty"; + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + openEdition.claim(receiver, 100, address(0), 0, alp, ""); + + vm.prank(deployer); + openEdition.setClaimConditions(conditions, true); + + vm.prank(getActor(5), getActor(5)); + openEdition.claim(receiver, 100, address(0), 0, alp, ""); + } + + /*/////////////////////////////////////////////////////////////// + setClaimConditions + //////////////////////////////////////////////////////////////*/ + + function test_claimCondition_startIdAndCount() public { + vm.startPrank(deployer); + + uint256 currentStartId = 0; + uint256 count = 0; + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](2); + conditions[0].startTimestamp = 0; + conditions[0].maxClaimableSupply = 10; + conditions[1].startTimestamp = 1; + conditions[1].maxClaimableSupply = 10; + + openEdition.setClaimConditions(conditions, false); + (currentStartId, count) = openEdition.claimCondition(); + assertEq(currentStartId, 0); + assertEq(count, 2); + + openEdition.setClaimConditions(conditions, false); + (currentStartId, count) = openEdition.claimCondition(); + assertEq(currentStartId, 0); + assertEq(count, 2); + + openEdition.setClaimConditions(conditions, true); + (currentStartId, count) = openEdition.claimCondition(); + assertEq(currentStartId, 2); + assertEq(count, 2); + + openEdition.setClaimConditions(conditions, true); + (currentStartId, count) = openEdition.claimCondition(); + assertEq(currentStartId, 4); + assertEq(count, 2); + } + + function test_claimCondition_startPhase() public { + vm.startPrank(deployer); + + uint256 activeConditionId = 0; + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](3); + conditions[0].startTimestamp = 10; + conditions[0].maxClaimableSupply = 11; + conditions[0].quantityLimitPerWallet = 12; + conditions[1].startTimestamp = 20; + conditions[1].maxClaimableSupply = 21; + conditions[1].quantityLimitPerWallet = 22; + conditions[2].startTimestamp = 30; + conditions[2].maxClaimableSupply = 31; + conditions[2].quantityLimitPerWallet = 32; + openEdition.setClaimConditions(conditions, false); + + vm.expectRevert("!CONDITION."); + openEdition.getActiveClaimConditionId(); + + vm.warp(10); + activeConditionId = openEdition.getActiveClaimConditionId(); + assertEq(activeConditionId, 0); + assertEq(openEdition.getClaimConditionById(activeConditionId).startTimestamp, 10); + assertEq(openEdition.getClaimConditionById(activeConditionId).maxClaimableSupply, 11); + assertEq(openEdition.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 12); + + vm.warp(20); + activeConditionId = openEdition.getActiveClaimConditionId(); + assertEq(activeConditionId, 1); + assertEq(openEdition.getClaimConditionById(activeConditionId).startTimestamp, 20); + assertEq(openEdition.getClaimConditionById(activeConditionId).maxClaimableSupply, 21); + assertEq(openEdition.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 22); + + vm.warp(30); + activeConditionId = openEdition.getActiveClaimConditionId(); + assertEq(activeConditionId, 2); + assertEq(openEdition.getClaimConditionById(activeConditionId).startTimestamp, 30); + assertEq(openEdition.getClaimConditionById(activeConditionId).maxClaimableSupply, 31); + assertEq(openEdition.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 32); + + vm.warp(40); + assertEq(openEdition.getActiveClaimConditionId(), 2); + } +} diff --git a/src/test/open-edition-flat-fee/_beforeTokenTransfers/_beforeTokenTransfers.t.sol b/src/test/open-edition-flat-fee/_beforeTokenTransfers/_beforeTokenTransfers.t.sol new file mode 100644 index 000000000..4ea304d13 --- /dev/null +++ b/src/test/open-edition-flat-fee/_beforeTokenTransfers/_beforeTokenTransfers.t.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { OpenEditionERC721FlatFee } from "contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "src/test/utils/BaseTest.sol"; + +contract OpenEditionERC721FlatFeeHarness is OpenEditionERC721FlatFee { + function beforeTokenTransfers(address from, address to, uint256 startTokenId_, uint256 quantity) public { + _beforeTokenTransfers(from, to, startTokenId_, quantity); + } +} + +contract OpenEditionERC721FlatFeeTest_beforeTokenTransfers is BaseTest { + OpenEditionERC721FlatFeeHarness public openEdition; + + address private openEditionImpl; + + function setUp() public override { + super.setUp(); + openEditionImpl = address(new OpenEditionERC721FlatFeeHarness()); + vm.prank(deployer); + openEdition = OpenEditionERC721FlatFeeHarness( + address( + new TWProxy( + openEditionImpl, + abi.encodeCall( + OpenEditionERC721FlatFee.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc + //////////////////////////////////////////////////////////////*/ + + function test_revert_transfersRestricted() public { + address from = address(0x1); + address to = address(0x2); + bytes32 role = keccak256("TRANSFER_ROLE"); + vm.prank(deployer); + openEdition.revokeRole(role, address(0)); + + vm.expectRevert(bytes("!T")); + openEdition.beforeTokenTransfers(from, to, 0, 1); + } +} diff --git a/src/test/open-edition-flat-fee/_beforeTokenTransfers/_beforeTokenTransfers.tree b/src/test/open-edition-flat-fee/_beforeTokenTransfers/_beforeTokenTransfers.tree new file mode 100644 index 000000000..078bd6a79 --- /dev/null +++ b/src/test/open-edition-flat-fee/_beforeTokenTransfers/_beforeTokenTransfers.tree @@ -0,0 +1,12 @@ +function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId_, + uint256 quantity +) +└── when address(0) does not have the transfer role + └── when from does not equal address(0) + └── when to does not equal address(0) + └── when from does not have the transfer role + └── when to does not have the transfer role + └── it should revert ✅ diff --git a/src/test/open-edition-flat-fee/_canSetFunctions/_canSetFunctions.t.sol b/src/test/open-edition-flat-fee/_canSetFunctions/_canSetFunctions.t.sol new file mode 100644 index 000000000..c45ca2514 --- /dev/null +++ b/src/test/open-edition-flat-fee/_canSetFunctions/_canSetFunctions.t.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { OpenEditionERC721FlatFee } from "contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "src/test/utils/BaseTest.sol"; + +contract OpenEditionERC721FlatFeeHarness is OpenEditionERC721FlatFee { + function canSetPrimarySaleRecipient() external view returns (bool) { + return _canSetPrimarySaleRecipient(); + } + + function canSetOwner() external view returns (bool) { + return _canSetOwner(); + } + + /// @dev Checks whether royalty info can be set in the given execution context. + function canSetRoyaltyInfo() external view returns (bool) { + return _canSetRoyaltyInfo(); + } + + /// @dev Checks whether contract metadata can be set in the given execution context. + function canSetContractURI() external view returns (bool) { + return _canSetContractURI(); + } + + /// @dev Checks whether platform fee info can be set in the given execution context. + function canSetClaimConditions() external view returns (bool) { + return _canSetClaimConditions(); + } + + /// @dev Returns whether the shared metadata of tokens can be set in the given execution context. + function canSetSharedMetadata() external view virtual returns (bool) { + return _canSetSharedMetadata(); + } +} + +contract OpenEditionERC721FlatFeeTest_canSetFunctions is BaseTest { + OpenEditionERC721FlatFeeHarness public openEdition; + + address private openEditionImpl; + + function setUp() public override { + super.setUp(); + openEditionImpl = address(new OpenEditionERC721FlatFeeHarness()); + vm.prank(deployer); + openEdition = OpenEditionERC721FlatFeeHarness( + address( + new TWProxy( + openEditionImpl, + abi.encodeCall( + OpenEditionERC721FlatFee.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc + //////////////////////////////////////////////////////////////*/ + + function test_canSetPrimarySaleRecipient_returnTrue() public { + vm.prank(deployer); + assertTrue(openEdition.canSetPrimarySaleRecipient()); + } + + function test_canSetPrimarySaleRecipient_returnFalse() public { + assertFalse(openEdition.canSetPrimarySaleRecipient()); + } + + function test_canSetOwner_returnTrue() public { + vm.prank(deployer); + assertTrue(openEdition.canSetOwner()); + } + + function test_canSetOwner_returnFalse() public { + assertFalse(openEdition.canSetOwner()); + } + + function test_canSetRoyaltyInfo_returnTrue() public { + vm.prank(deployer); + assertTrue(openEdition.canSetRoyaltyInfo()); + } + + function test_canSetRoyaltyInfo_returnFalse() public { + assertFalse(openEdition.canSetRoyaltyInfo()); + } + + function test_canSetContractURI_returnTrue() public { + vm.prank(deployer); + assertTrue(openEdition.canSetContractURI()); + } + + function test_canSetContractURI_returnFalse() public { + assertFalse(openEdition.canSetContractURI()); + } + + function test_canSetClaimConditions_returnTrue() public { + vm.prank(deployer); + assertTrue(openEdition.canSetClaimConditions()); + } + + function test_canSetClaimConditions_returnFalse() public { + assertFalse(openEdition.canSetClaimConditions()); + } + + function test_canSetSharedMetadata_returnTrue() public { + vm.prank(deployer); + assertTrue(openEdition.canSetSharedMetadata()); + } + + function test_canSetSharedMetadata_returnFalse() public { + assertFalse(openEdition.canSetSharedMetadata()); + } +} diff --git a/src/test/open-edition-flat-fee/_canSetFunctions/_canSetFunctions.tree b/src/test/open-edition-flat-fee/_canSetFunctions/_canSetFunctions.tree new file mode 100644 index 000000000..1ccb478fd --- /dev/null +++ b/src/test/open-edition-flat-fee/_canSetFunctions/_canSetFunctions.tree @@ -0,0 +1,39 @@ +function _canSetPrimarySaleRecipient() +├── when _msgSender has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when _msgSender does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetOwner() +├── when _msgSender has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when _msgSender does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + + +function _canSetRoyaltyInfo() +├── when _msgSender has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when _msgSender does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + + +function _canSetContractURI() +├── when _msgSender has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when _msgSender does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + + +function _canSetClaimConditions() +├── when _msgSender has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when _msgSender does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + + +function _canSetSharedMetadata() +├── when _msgSender has minter role +│ └── it should return true ✅ +└── when _msgSender does not have minter role + └── it should return false ✅ diff --git a/src/test/open-edition-flat-fee/_collectPriceOnClaim/_collectPriceOnClaim.t.sol b/src/test/open-edition-flat-fee/_collectPriceOnClaim/_collectPriceOnClaim.t.sol new file mode 100644 index 000000000..bc165f600 --- /dev/null +++ b/src/test/open-edition-flat-fee/_collectPriceOnClaim/_collectPriceOnClaim.t.sol @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { OpenEditionERC721FlatFee } from "contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "src/test/utils/BaseTest.sol"; + +contract OpenEditionERC721FlatFeeHarness is OpenEditionERC721FlatFee { + function collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) external payable { + _collectPriceOnClaim(_primarySaleRecipient, _quantityToClaim, _currency, _pricePerToken); + } +} + +contract OpenEditionERC721FlatFeeTest_collectPrice is BaseTest { + OpenEditionERC721FlatFeeHarness public openEdition; + + address private openEditionImpl; + + address private currency; + address private primarySaleRecipient; + uint256 private msgValue; + uint256 private pricePerToken; + uint256 private qty = 1; + + function setUp() public override { + super.setUp(); + openEditionImpl = address(new OpenEditionERC721FlatFeeHarness()); + vm.prank(deployer); + openEdition = OpenEditionERC721FlatFeeHarness( + address( + new TWProxy( + openEditionImpl, + abi.encodeCall( + OpenEditionERC721FlatFee.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc + //////////////////////////////////////////////////////////////*/ + + modifier pricePerTokenZero() { + _; + } + + modifier pricePerTokenNotZero() { + pricePerToken = 1 ether; + _; + } + + modifier msgValueZero() { + _; + } + + modifier msgValueNotZero() { + msgValue = 1 ether; + _; + } + + modifier valuePriceMismatch() { + msgValue = 1 ether; + pricePerToken = 2 ether; + _; + } + + modifier primarySaleRecipientZeroAddress() { + primarySaleRecipient = address(0); + _; + } + + modifier primarySaleRecipientNotZeroAddress() { + primarySaleRecipient = address(0x0999); + _; + } + + modifier currencyNativeToken() { + currency = NATIVE_TOKEN; + _; + } + + modifier currencyNotNativeToken() { + currency = address(erc20); + _; + } + + function test_revert_pricePerTokenZeroMsgValueNotZero() public pricePerTokenZero msgValueNotZero { + vm.expectRevert("!Value"); + openEdition.collectPriceOnClaim{ value: msgValue }(primarySaleRecipient, qty, currency, pricePerToken); + } + + function test_revert_nativeCurrencyValuePriceMismatch() public currencyNativeToken valuePriceMismatch { + vm.expectRevert(bytes("!V")); + openEdition.collectPriceOnClaim{ value: msgValue }(primarySaleRecipient, qty, currency, pricePerToken); + } + + function test_revert_erc20ValuePriceMismatch() public currencyNotNativeToken valuePriceMismatch { + vm.expectRevert(bytes("!V")); + openEdition.collectPriceOnClaim{ value: msgValue }(primarySaleRecipient, qty, currency, pricePerToken); + } + + function test_state_nativeCurrency() + public + currencyNativeToken + pricePerTokenNotZero + msgValueNotZero + primarySaleRecipientNotZeroAddress + { + uint256 beforeBalancePrimarySaleRecipient = address(primarySaleRecipient).balance; + + openEdition.collectPriceOnClaim{ value: msgValue }(primarySaleRecipient, qty, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = address(primarySaleRecipient).balance; + + uint256 platformFeeVal = (msgValue * platformFeeBps) / 10_000; + uint256 primarySaleRecipientVal = msgValue - platformFeeVal; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + } + + function test_revert_erc20_msgValueNotZero() + public + currencyNotNativeToken + msgValueNotZero + primarySaleRecipientNotZeroAddress + { + vm.expectRevert("!Value"); + openEdition.collectPriceOnClaim{ value: msgValue }(primarySaleRecipient, qty, currency, pricePerToken); + } + + function test_state_erc20() public currencyNotNativeToken pricePerTokenNotZero primarySaleRecipientNotZeroAddress { + erc20.mint(address(this), pricePerToken); + ERC20(erc20).approve(address(openEdition), pricePerToken); + uint256 beforeBalancePrimarySaleRecipient = erc20.balanceOf(primarySaleRecipient); + + openEdition.collectPriceOnClaim(primarySaleRecipient, qty, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = erc20.balanceOf(primarySaleRecipient); + + uint256 platformFeeVal = (1 ether * platformFeeBps) / 10_000; + uint256 primarySaleRecipientVal = 1 ether - platformFeeVal; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + } + + function test_state_erc20StoredPrimarySaleRecipient() + public + currencyNotNativeToken + pricePerTokenNotZero + primarySaleRecipientZeroAddress + { + address storedPrimarySaleRecipient = openEdition.primarySaleRecipient(); + + erc20.mint(address(this), pricePerToken); + ERC20(erc20).approve(address(openEdition), pricePerToken); + uint256 beforeBalancePrimarySaleRecipient = erc20.balanceOf(storedPrimarySaleRecipient); + + openEdition.collectPriceOnClaim(primarySaleRecipient, qty, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = erc20.balanceOf(storedPrimarySaleRecipient); + + uint256 platformFeeVal = (1 ether * platformFeeBps) / 10_000; + uint256 primarySaleRecipientVal = 1 ether - platformFeeVal; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + } + + function test_state_nativeCurrencyStoredPrimarySaleRecipient() + public + currencyNativeToken + pricePerTokenNotZero + primarySaleRecipientZeroAddress + msgValueNotZero + { + address storedPrimarySaleRecipient = openEdition.primarySaleRecipient(); + + uint256 beforeBalancePrimarySaleRecipient = address(storedPrimarySaleRecipient).balance; + + openEdition.collectPriceOnClaim{ value: msgValue }(primarySaleRecipient, qty, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = address(storedPrimarySaleRecipient).balance; + + uint256 platformFeeVal = (msgValue * platformFeeBps) / 10_000; + uint256 primarySaleRecipientVal = msgValue - platformFeeVal; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + } +} diff --git a/src/test/open-edition-flat-fee/_collectPriceOnClaim/_collectPriceOnClaim.tree b/src/test/open-edition-flat-fee/_collectPriceOnClaim/_collectPriceOnClaim.tree new file mode 100644 index 000000000..2054cf049 --- /dev/null +++ b/src/test/open-edition-flat-fee/_collectPriceOnClaim/_collectPriceOnClaim.tree @@ -0,0 +1,37 @@ +function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken +) +├── when _pricePerToken is equal to zero +│ ├── when msg.value does not equal to zero +│ │ └── it should revert ✅ +│ └── when msg.value is equal to zero +│ └── it should return ✅ +└── when _pricePerToken is not equal to zero + ├── when _primarySaleRecipient is equal to address(0) + │ ├── when saleRecipient for _tokenId is equal to address(0) + │ │ ├── when currency is native token + │ │ │ ├── when msg.value does not equal totalPrice + │ │ │ │ └── it should revert ✅ + │ │ │ └── when msg.value does equal totalPrice + │ │ │ └── it should transfer totalPrice to primarySaleRecipient in native token ✅ + │ │ └── when currency is not native token + │ │ └── it should transfer totalPrice to primarySaleRecipient in _currency token ✅ + │ └── when salerecipient for _tokenId is not equal to address(0) + │ ├── when currency is native token + │ │ ├── when msg.value does not equal totalPrice + │ │ │ └── it should revert ✅ + │ │ └── when msg.value does equal totalPrice + │ │ └── it should transfer totalPrice to saleRecipient for _tokenId in native token ✅ + │ └── when currency is not native token + │ └── it should transfer totalPrice to saleRecipient for _tokenId in _currency token ✅ + └── when _primarySaleRecipient is not equal to address(0) + ├── when currency is native token + │ ├── when msg.value does not equal totalPrice + │ │ └── it should revert ✅ + │ └── when msg.value does equal totalPrice + │ └── it should transfer totalPrice to _primarySaleRecipient in native token ✅ + └── when currency is not native token + └── it should transfer totalPrice to _primarySaleRecipient in _currency token ✅ \ No newline at end of file diff --git a/src/test/open-edition-flat-fee/_transferTokensOnClaim/_transferTokensOnClaim.t.sol b/src/test/open-edition-flat-fee/_transferTokensOnClaim/_transferTokensOnClaim.t.sol new file mode 100644 index 000000000..681ca6caa --- /dev/null +++ b/src/test/open-edition-flat-fee/_transferTokensOnClaim/_transferTokensOnClaim.t.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { OpenEditionERC721FlatFee, IERC721AUpgradeable } from "contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "src/test/utils/BaseTest.sol"; + +contract OpenEditionERC721FlatFeeHarness is OpenEditionERC721FlatFee { + function transferTokensOnClaim(address _to, uint256 quantityBeingClaimed) public { + _transferTokensOnClaim(_to, quantityBeingClaimed); + } +} + +contract MockERC721Receiver { + function onERC721Received(address, address, uint256, bytes memory) external pure returns (bytes4) { + return this.onERC721Received.selector; + } +} + +contract MockERC721NotReceiver {} + +contract OpenEditionERC721FlatFeeTest_transferTokensOnClaim is BaseTest { + OpenEditionERC721FlatFeeHarness public openEdition; + + MockERC721NotReceiver private notReceiver; + MockERC721Receiver private receiver; + + address private openEditionImpl; + + function setUp() public override { + super.setUp(); + openEditionImpl = address(new OpenEditionERC721FlatFeeHarness()); + vm.prank(deployer); + openEdition = OpenEditionERC721FlatFeeHarness( + address( + new TWProxy( + openEditionImpl, + abi.encodeCall( + OpenEditionERC721FlatFee.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ); + + receiver = new MockERC721Receiver(); + notReceiver = new MockERC721NotReceiver(); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc + //////////////////////////////////////////////////////////////*/ + + function test_revert_TransferToNonReceiverContract() public { + vm.expectRevert(IERC721AUpgradeable.TransferToNonERC721ReceiverImplementer.selector); + openEdition.transferTokensOnClaim(address(notReceiver), 1); + } + + function test_state_transferToReceiverContract() public { + uint256 receiverBalanceBefore = openEdition.balanceOf(address(receiver)); + uint256 nextTokenToMintBefore = openEdition.nextTokenIdToMint(); + + openEdition.transferTokensOnClaim(address(receiver), 1); + + uint256 receiverBalanceAfter = openEdition.balanceOf(address(receiver)); + uint256 nextTokenToMintAfter = openEdition.nextTokenIdToMint(); + + assertEq(receiverBalanceAfter, receiverBalanceBefore + 1); + assertEq(nextTokenToMintAfter, nextTokenToMintBefore + 1); + } + + function test_state_transferToEOA() public { + address to = address(0x01); + uint256 receiverBalanceBefore = openEdition.balanceOf(to); + uint256 nextTokenToMintBefore = openEdition.nextTokenIdToMint(); + + openEdition.transferTokensOnClaim(to, 1); + + uint256 receiverBalanceAfter = openEdition.balanceOf(to); + uint256 nextTokenToMintAfter = openEdition.nextTokenIdToMint(); + + assertEq(receiverBalanceAfter, receiverBalanceBefore + 1); + assertEq(nextTokenToMintAfter, nextTokenToMintBefore + 1); + } +} diff --git a/src/test/open-edition-flat-fee/_transferTokensOnClaim/_transferTokensOnClaim.tree b/src/test/open-edition-flat-fee/_transferTokensOnClaim/_transferTokensOnClaim.tree new file mode 100644 index 000000000..bddcf87f6 --- /dev/null +++ b/src/test/open-edition-flat-fee/_transferTokensOnClaim/_transferTokensOnClaim.tree @@ -0,0 +1,8 @@ +function _transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) +├── when _to is a smart contract +│ ├── when _to has not implemented ERC721Receiver +│ │ └── it should revert ✅ +│ └── when _to has implemented ERC721Receiver +│ └── it should mint _quantityBeingClaimed tokens to _to ✅ +└── when _to is an EOA + └── it should mint _quantityBeingClaimed tokens to _to ✅ \ No newline at end of file diff --git a/src/test/open-edition-flat-fee/initialize/initialize.t.sol b/src/test/open-edition-flat-fee/initialize/initialize.t.sol new file mode 100644 index 000000000..61c1f3a22 --- /dev/null +++ b/src/test/open-edition-flat-fee/initialize/initialize.t.sol @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { OpenEditionERC721FlatFee } from "contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "src/test/utils/BaseTest.sol"; + +contract OpenEditionERC721FlatFeeTest_initialize is BaseTest { + event ContractURIUpdated(string prevURI, string newURI); + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + event PrimarySaleRecipientUpdated(address indexed recipient); + + OpenEditionERC721FlatFee public openEdition; + + address private openEditionImpl; + + function deployOpenEdition( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient, + address _imp + ) public { + vm.prank(deployer); + openEdition = OpenEditionERC721FlatFee( + address( + new TWProxy( + _imp, + abi.encodeCall( + OpenEditionERC721FlatFee.initialize, + ( + _defaultAdmin, + _name, + _symbol, + _contractURI, + _trustedForwarders, + _saleRecipient, + _royaltyRecipient, + _royaltyBps, + _platformFeeBps, + _platformFeeRecipient + ) + ) + ) + ) + ); + } + + function setUp() public override { + super.setUp(); + openEditionImpl = address(new OpenEditionERC721FlatFee()); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: initialize + //////////////////////////////////////////////////////////////*/ + + function test_state() public { + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient, + openEditionImpl + ); + + address _saleRecipient = openEdition.primarySaleRecipient(); + (address _royaltyRecipient, uint16 _royaltyBps) = openEdition.getDefaultRoyaltyInfo(); + string memory _name = openEdition.name(); + string memory _symbol = openEdition.symbol(); + string memory _contractURI = openEdition.contractURI(); + address _owner = openEdition.owner(); + + assertEq(_name, NAME); + assertEq(_symbol, SYMBOL); + assertEq(_contractURI, CONTRACT_URI); + assertEq(_saleRecipient, saleRecipient); + assertEq(_royaltyRecipient, royaltyRecipient); + assertEq(_royaltyBps, royaltyBps); + assertEq(_owner, deployer); + + for (uint256 i = 0; i < forwarders().length; i++) { + assertEq(openEdition.isTrustedForwarder(forwarders()[i]), true); + } + + assertTrue(openEdition.hasRole(openEdition.DEFAULT_ADMIN_ROLE(), deployer)); + assertTrue(openEdition.hasRole(keccak256("TRANSFER_ROLE"), deployer)); + assertTrue(openEdition.hasRole(keccak256("MINTER_ROLE"), deployer)); + assertTrue(openEdition.hasRole(keccak256("TRANSFER_ROLE"), address(0))); + } + + function test_revert_RoyaltyTooHigh() public { + uint128 _royaltyBps = 10001; + + vm.expectRevert("Exceeds max bps"); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + _royaltyBps, + platformFeeBps, + platformFeeRecipient, + openEditionImpl + ); + } + + function test_event_ContractURIUpdated() public { + vm.expectEmit(false, false, false, true); + emit ContractURIUpdated("", CONTRACT_URI); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient, + openEditionImpl + ); + } + + function test_event_OwnerUpdated() public { + vm.expectEmit(true, true, false, false); + emit OwnerUpdated(address(0), deployer); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient, + openEditionImpl + ); + } + + function test_event_TransferRoleAddressZero() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + vm.expectEmit(true, true, false, false); + emit RoleGranted(role, address(0), deployer); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient, + openEditionImpl + ); + } + + function test_event_TransferRoleAdmin() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + vm.expectEmit(true, true, false, false); + emit RoleGranted(role, deployer, deployer); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient, + openEditionImpl + ); + } + + function test_event_MinterRoleAdmin() public { + bytes32 role = keccak256("MINTER_ROLE"); + vm.expectEmit(true, true, false, false); + emit RoleGranted(role, deployer, deployer); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient, + openEditionImpl + ); + } + + function test_event_DefaultAdminRoleAdmin() public { + bytes32 role = bytes32(0x00); + vm.expectEmit(true, true, false, false); + emit RoleGranted(role, deployer, deployer); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient, + openEditionImpl + ); + } + + function test_event_PrimarysaleRecipientUpdated() public { + vm.expectEmit(true, false, false, false); + emit PrimarySaleRecipientUpdated(saleRecipient); + deployOpenEdition( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient, + openEditionImpl + ); + } +} diff --git a/src/test/open-edition-flat-fee/initialize/initialize.tree b/src/test/open-edition-flat-fee/initialize/initialize.tree new file mode 100644 index 000000000..f56ad144b --- /dev/null +++ b/src/test/open-edition-flat-fee/initialize/initialize.tree @@ -0,0 +1,39 @@ +function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _royaltyRecipient, + uint128 _royaltyBps, + uint128 _platformFeeBps, + address _platformFeeRecipient +) +├── when _trustedForwarders.length > 0 +│ └── it should set _trustedForwarder[_trustedForwarders[i]] as true for each address in _trustedForwarders ✅ +├── it should set _name as the value provided in _name ✅ +├── it should set _symbol as the value provided in _symbol ✅ +├── it should set _currentIndex as 0 ✅ +├── it should set contractURI as _contractURI ✅ +├── it should emit ContractURIUpdated with the parameters: prevURI, _uri ✅ +├── it should set _defaultAdmin as the owner of the contract ✅ +├── it should emit OwnerUpdated with the parameters: _prevOwner, _defaultAdmin ✅ +├── it should assign the role DEFAULT_ADMIN_ROLE to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: DEFAULT_ADMIN_ROLE, _defaultAdmin, msg.sender ✅ +├── it should assign the role _minterRole to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: _minterRole, _defaultAdmin, msg.sender ✅ +├── it should assign the role _transferRole to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: _transferRole, _defaultAdmin, msg.sender ✅ +├── it should assign the role _transferRole to address(0) ✅ +├── it should emit RoleGranted with the parameters: _transferRole, address(0), msg.sender ✅ +├── when _royaltyBps is greater than 10_000 +│ └── it should revert ✅ +├── when _royaltyBps is less than or equal to 10_000 +│ ├── it should set royaltyRecipient as _royaltyRecipient ✅ +│ ├── it should set royaltyBps as uint16(_royaltyBps) ✅ +│ └── it should emit DefaultRoyalty with the parameters _royaltyRecipient, _royaltyBps +├── it should set recipient as _primarySaleRecipient ✅ +├── it should emit PrimarySaleRecipientUpdated with the parameters _primarySaleRecipient ✅ +├── it should set transferRole as keccak256("TRANSFER_ROLE") ✅ +└── it should set minterRole as keccak256("MINTER_ROLE") ✅ diff --git a/src/test/open-edition-flat-fee/misc/misc.t.sol b/src/test/open-edition-flat-fee/misc/misc.t.sol new file mode 100644 index 000000000..760373600 --- /dev/null +++ b/src/test/open-edition-flat-fee/misc/misc.t.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { IERC721AUpgradeable, OpenEditionERC721FlatFee, ISharedMetadata } from "contracts/prebuilts/open-edition/OpenEditionERC721FlatFee.sol"; +import { NFTMetadataRenderer } from "contracts/lib/NFTMetadataRenderer.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "src/test/utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; + +contract HarnessOpenEditionERC721FlatFee is OpenEditionERC721FlatFee { + function msgData() public view returns (bytes memory) { + return _msgData(); + } +} + +contract OpenEditionERC721FlatFeeTest_misc is BaseTest { + OpenEditionERC721FlatFee public openEdition; + HarnessOpenEditionERC721FlatFee public harnessOpenEdition; + + address private openEditionImpl; + address private harnessImpl; + + address private receiver = 0xDDdDddDdDdddDDddDDddDDDDdDdDDdDDdDDDDDDd; + + ISharedMetadata.SharedMetadataInfo public sharedMetadata; + + function setUp() public override { + super.setUp(); + openEditionImpl = address(new OpenEditionERC721FlatFee()); + vm.prank(deployer); + openEdition = OpenEditionERC721FlatFee( + address( + new TWProxy( + openEditionImpl, + abi.encodeCall( + OpenEditionERC721FlatFee.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ); + + sharedMetadata = ISharedMetadata.SharedMetadataInfo({ + name: "Test", + description: "Test", + imageURI: "https://test.com", + animationURI: "https://test.com" + }); + } + + function deployHarness() internal { + harnessImpl = address(new HarnessOpenEditionERC721FlatFee()); + harnessOpenEdition = HarnessOpenEditionERC721FlatFee( + address( + new TWProxy( + harnessImpl, + abi.encodeCall( + OpenEditionERC721FlatFee.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: misc + //////////////////////////////////////////////////////////////*/ + + modifier claimTokens() { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = "300"; + inputs[3] = "0"; + inputs[4] = Strings.toHexString(uint160(address(erc20))); // address of erc20 + + bytes memory result = vm.ffi(inputs); + bytes32 root = abi.decode(result, (bytes32)); + + inputs[1] = "src/test/scripts/getProof.ts"; + result = vm.ffi(inputs); + bytes32[] memory proofs = abi.decode(result, (bytes32[])); + + OpenEditionERC721FlatFee.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + vm.warp(1); + + OpenEditionERC721FlatFee.ClaimCondition[] memory conditions = new OpenEditionERC721FlatFee.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 10; + conditions[0].merkleRoot = root; + conditions[0].pricePerToken = 10; + conditions[0].currency = address(erc20); + + vm.prank(deployer); + openEdition.setClaimConditions(conditions, false); + + vm.prank(receiver, receiver); + openEdition.claim(receiver, 100, address(erc20), 0, alp, ""); + _; + } + + modifier callerOwner() { + vm.startPrank(receiver); + _; + } + + modifier callerNotOwner() { + _; + } + + function test_tokenURI_revert_tokenDoesNotExist() public { + vm.expectRevert(bytes("!ID")); + openEdition.tokenURI(1); + } + + function test_tokenURI_returnMetadata() public claimTokens { + vm.prank(deployer); + openEdition.setSharedMetadata(sharedMetadata); + + string memory uri = openEdition.tokenURI(1); + assertEq( + uri, + NFTMetadataRenderer.createMetadataEdition({ + name: sharedMetadata.name, + description: sharedMetadata.description, + imageURI: sharedMetadata.imageURI, + animationURI: sharedMetadata.animationURI, + tokenOfEdition: 1 + }) + ); + } + + function test_startTokenId_returnOne() public { + assertEq(openEdition.startTokenId(), 1); + } + + function test_totalMinted_returnZero() public { + assertEq(openEdition.totalMinted(), 0); + } + + function test_totalMinted_returnOneHundred() public claimTokens { + assertEq(openEdition.totalMinted(), 100); + } + + function test_nextTokenIdToMint_returnOne() public { + assertEq(openEdition.nextTokenIdToMint(), 1); + } + + function test_nextTokenIdToMint_returnOneHundredAndOne() public claimTokens { + assertEq(openEdition.nextTokenIdToMint(), 101); + } + + function test_nextTokenIdToClaim_returnOne() public { + assertEq(openEdition.nextTokenIdToClaim(), 1); + } + + function test_nextTokenIdToClaim_returnOneHundredAndOne() public claimTokens { + assertEq(openEdition.nextTokenIdToClaim(), 101); + } + + function test_burn_revert_callerNotOwner() public claimTokens callerNotOwner { + vm.expectRevert(IERC721AUpgradeable.TransferCallerNotOwnerNorApproved.selector); + openEdition.burn(1); + } + + function test_burn_state_callerOwner() public claimTokens callerOwner { + uint256 balanceBeforeBurn = openEdition.balanceOf(receiver); + + openEdition.burn(1); + + uint256 balanceAfterBurn = openEdition.balanceOf(receiver); + + assertEq(balanceBeforeBurn - balanceAfterBurn, 1); + } + + function test_burn_state_callerApproved() public claimTokens { + uint256 balanceBeforeBurn = openEdition.balanceOf(receiver); + + vm.prank(receiver); + openEdition.setApprovalForAll(deployer, true); + + vm.prank(deployer); + openEdition.burn(1); + + uint256 balanceAfterBurn = openEdition.balanceOf(receiver); + + assertEq(balanceBeforeBurn - balanceAfterBurn, 1); + } + + function test_supportsInterface() public { + assertEq(openEdition.supportsInterface(type(IERC2981Upgradeable).interfaceId), true); + bytes4 invalidId = bytes4(0); + assertEq(openEdition.supportsInterface(invalidId), false); + } + + function test_msgData_returnValue() public { + deployHarness(); + bytes memory msgData = harnessOpenEdition.msgData(); + bytes4 expectedData = harnessOpenEdition.msgData.selector; + assertEq(bytes4(msgData), expectedData); + } +} diff --git a/src/test/open-edition-flat-fee/misc/misc.tree b/src/test/open-edition-flat-fee/misc/misc.tree new file mode 100644 index 000000000..07abb950c --- /dev/null +++ b/src/test/open-edition-flat-fee/misc/misc.tree @@ -0,0 +1,33 @@ +function tokenURI(uint256 _tokenId) +├── when _tokenId does not exist +│ └── it should revert ✅ +└── when _tokenID does exist + └── it should return the shared metadata ✅ + +function supportsInterface(bytes4 interfaceId) +├── it should return true for any of the listed interface ids ✅ +└── it should return false for any interfaces ids that are not listed ✅ + +function _startTokenId() +└── it should return 1 ✅ + +function startTokenId() +└── it should return _startTokenId (1) ✅ + +function totalminted() +└── it should return the total number of NFTs minted ✅ + +function nextTokenIdToMint() +└── it should return the next token ID to mint (last minted + 1) ✅ + +function nextTokenIdToClaim() +└── it should return the next token ID to mint (last minted + 1) ✅ + +function burn(uint256 tokenId) +├── when caller is not the owner of tokenId +│ ├── when caller is not an approved operator of the owner of tokenId +│ │ └── it should revert ✅ +│ └── when caller is an approved operator of the owner of tokenId +│ └── it should burn the token ✅ +└── when caller is the owner of tokenId + └── it should burn the token ✅ \ No newline at end of file