From 987c765d805e09d0cd73460419a4a9973e37847b Mon Sep 17 00:00:00 2001 From: Yash <72552910+kumaryash90@users.noreply.github.com> Date: Tue, 17 Oct 2023 04:57:57 +0530 Subject: [PATCH] BTT BurnToClaimDropERC721 and extensions (#509) * test verifyBurnToClaim * Fix [Q-2] Missing sanity check when setting BurnToClaimInfo * test setBurnToClaimInfo * fix tests * test _burnTokensOnOrigin * test _burnTokensOnOrigin * test verifyClaim * test wallet random * test setClaimConditions * test getActiveClaimConditionId * make verifyClaim virtual * test claim * test setContractURI * update as per latest audit for burn-to-claim * test royalty extension * test ownable extension * test delayed-reveal extension * test batch-mint-metadata extension * test lazy-mint extension * test initialize burn-to-claim * test other functions burn-to-claim * test lazyMint main contract * test burnAndClaim main contract * test reveal main contract * other function tests * mark checked * other function tests * fix * fix --------- Co-authored-by: Krishang Co-authored-by: Joaquim Verges --- contracts/extension/BurnToClaim.sol | 4 + contracts/extension/Drop.sol | 2 +- contracts/extension/Drop1155.sol | 2 +- contracts/extension/DropSinglePhase.sol | 2 +- contracts/extension/DropSinglePhase1155.sol | 2 +- contracts/extension/upgradeable/Drop.sol | 2 +- .../BurnToClaimDropERC721.t.sol | 1884 +++++++++++++++++ .../logic/burn-and-claim/burnAndClaim.t.sol | 802 +++++++ .../logic/burn-and-claim/burnAndClaim.tree | 83 + .../logic/lazy-mint/lazyMint.t.sol | 265 +++ .../logic/lazy-mint/lazyMint.tree | 38 + .../logic/other-functions/other.t.sol | 350 +++ .../logic/other-functions/other.tree | 84 + .../logic/reveal/reveal.t.sol | 242 +++ .../logic/reveal/reveal.tree | 16 + .../router/initialize/initialize.t.sol | 455 ++++ .../router/initialize/initialize.tree | 44 + .../router/other-functions/other.t.sol | 79 + .../router/other-functions/other.tree | 12 + .../drop-erc1155/initialize/initialize.t.sol | 10 +- .../drop-erc20/initialize/initialize.t.sol | 6 +- .../drop-erc721/initalizer/initializer.t.sol | 10 +- src/test/mocks/MockERC1155NonBurnable.sol | 28 + src/test/mocks/MockERC721NonBurnable.sol | 24 + src/test/scripts/generateRoot.ts | 4 +- src/test/scripts/getProof.ts | 4 +- src/test/sdk/extension/ExtensionUtilTest.sol | 133 ++ .../_batchMintMetadata.t.sol | 52 + .../_batchMintMetadata.tree | 7 + .../freeze-base-uri/_freezeBaseURI.t.sol | 71 + .../freeze-base-uri/_freezeBaseURI.tree | 6 + .../get-base-uri/_getBaseURI.t.sol | 67 + .../get-base-uri/_getBaseURI.tree | 6 + .../get-batch-id/_getBatchId.t.sol | 65 + .../get-batch-id/_getBatchId.tree | 6 + .../get-batch-start-id/_getBatchStartId.t.sol | 62 + .../get-batch-start-id/_getBatchStartId.tree | 6 + .../set-base-uri/_setBaseURI.t.sol | 84 + .../set-base-uri/_setBaseURI.tree | 6 + .../_burnTokensOnOrigin.t.sol | 130 ++ .../_burnTokensOnOrigin.tree | 15 + .../setBurnToClaimInfo.t.sol | 87 + .../setBurnToClaimInfo.tree | 11 + .../verifyBurnToClaim.t.sol | 146 ++ .../verifyBurnToClaim.tree | 23 + .../set-contract-uri/setContractURI.t.sol | 65 + .../set-contract-uri/setContractURI.tree | 6 + .../get-reveal-uri/getRevealURI.t.sol | 66 + .../get-reveal-uri/getRevealURI.tree | 8 + .../_setEncryptedData.t.sol | 37 + .../set-encrypted-data/_setEncryptedData.tree | 3 + src/test/sdk/extension/drop/claim/claim.t.sol | 107 + src/test/sdk/extension/drop/claim/claim.tree | 15 + .../getActiveClaimConditionId.t.sol | 118 ++ .../getActiveClaimConditionId.tree | 8 + .../setClaimConditions.t.sol | 380 ++++ .../setClaimConditions.tree | 24 + .../drop/verify-claim/verifyClaim.t.sol | 384 ++++ .../drop/verify-claim/verifyClaim.tree | 67 + .../lazy-mint/lazy-mint/lazyMint.t.sol | 119 ++ .../lazy-mint/lazy-mint/lazyMint.tree | 17 + .../ownable/set-owner/setOwner.t.sol | 72 + .../extension/ownable/set-owner/setOwner.tree | 6 + .../setDefaultRoyaltyInfo.t.sol | 94 + .../setDefaultRoyaltyInfo.tree | 11 + .../setRoyaltyInfoForToken.t.sol | 108 + .../setRoyaltyInfoForToken.tree | 15 + .../_batchMintMetadata.t.sol | 52 + .../_batchMintMetadata.tree | 7 + .../freeze-base-uri/_freezeBaseURI.t.sol | 75 + .../freeze-base-uri/_freezeBaseURI.tree | 6 + .../get-base-uri/_getBaseURI.t.sol | 67 + .../get-base-uri/_getBaseURI.tree | 6 + .../get-batch-id/_getBatchId.t.sol | 65 + .../get-batch-id/_getBatchId.tree | 6 + .../get-batch-start-id/_getBatchStartId.t.sol | 62 + .../get-batch-start-id/_getBatchStartId.tree | 6 + .../set-base-uri/_setBaseURI.t.sol | 84 + .../set-base-uri/_setBaseURI.tree | 6 + .../_burnTokensOnOrigin.t.sol | 130 ++ .../_burnTokensOnOrigin.tree | 15 + .../setBurnToClaimInfo.t.sol | 87 + .../setBurnToClaimInfo.tree | 11 + .../verifyBurnToClaim.t.sol | 146 ++ .../verifyBurnToClaim.tree | 23 + .../set-contract-uri/setContractURI.t.sol | 65 + .../set-contract-uri/setContractURI.tree | 6 + .../get-reveal-uri/getRevealURI.t.sol | 66 + .../get-reveal-uri/getRevealURI.tree | 8 + .../_setEncryptedData.t.sol | 37 + .../set-encrypted-data/_setEncryptedData.tree | 3 + .../upgradeable/drop/claim/claim.t.sol | 107 + .../upgradeable/drop/claim/claim.tree | 15 + .../getActiveClaimConditionId.t.sol | 118 ++ .../getActiveClaimConditionId.tree | 8 + .../setClaimConditions.t.sol | 380 ++++ .../setClaimConditions.tree | 24 + .../drop/verify-claim/verifyClaim.t.sol | 384 ++++ .../drop/verify-claim/verifyClaim.tree | 67 + .../lazy-mint/lazy-mint/lazyMint.t.sol | 119 ++ .../lazy-mint/lazy-mint/lazyMint.tree | 17 + .../ownable/set-owner/setOwner.t.sol | 72 + .../ownable/set-owner/setOwner.tree | 6 + .../setDefaultRoyaltyInfo.t.sol | 94 + .../setDefaultRoyaltyInfo.tree | 11 + .../setRoyaltyInfoForToken.t.sol | 108 + .../setRoyaltyInfoForToken.tree | 15 + src/test/utils/BaseTest.sol | 6 + 108 files changed, 9735 insertions(+), 22 deletions(-) create mode 100644 src/test/burn-to-claim-drop-BTT/BurnToClaimDropERC721.t.sol create mode 100644 src/test/burn-to-claim-drop-BTT/logic/burn-and-claim/burnAndClaim.t.sol create mode 100644 src/test/burn-to-claim-drop-BTT/logic/burn-and-claim/burnAndClaim.tree create mode 100644 src/test/burn-to-claim-drop-BTT/logic/lazy-mint/lazyMint.t.sol create mode 100644 src/test/burn-to-claim-drop-BTT/logic/lazy-mint/lazyMint.tree create mode 100644 src/test/burn-to-claim-drop-BTT/logic/other-functions/other.t.sol create mode 100644 src/test/burn-to-claim-drop-BTT/logic/other-functions/other.tree create mode 100644 src/test/burn-to-claim-drop-BTT/logic/reveal/reveal.t.sol create mode 100644 src/test/burn-to-claim-drop-BTT/logic/reveal/reveal.tree create mode 100644 src/test/burn-to-claim-drop-BTT/router/initialize/initialize.t.sol create mode 100644 src/test/burn-to-claim-drop-BTT/router/initialize/initialize.tree create mode 100644 src/test/burn-to-claim-drop-BTT/router/other-functions/other.t.sol create mode 100644 src/test/burn-to-claim-drop-BTT/router/other-functions/other.tree create mode 100644 src/test/mocks/MockERC1155NonBurnable.sol create mode 100644 src/test/mocks/MockERC721NonBurnable.sol create mode 100644 src/test/sdk/extension/ExtensionUtilTest.sol create mode 100644 src/test/sdk/extension/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.t.sol create mode 100644 src/test/sdk/extension/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.tree create mode 100644 src/test/sdk/extension/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.t.sol create mode 100644 src/test/sdk/extension/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.tree create mode 100644 src/test/sdk/extension/batch-mint-metadata/get-base-uri/_getBaseURI.t.sol create mode 100644 src/test/sdk/extension/batch-mint-metadata/get-base-uri/_getBaseURI.tree create mode 100644 src/test/sdk/extension/batch-mint-metadata/get-batch-id/_getBatchId.t.sol create mode 100644 src/test/sdk/extension/batch-mint-metadata/get-batch-id/_getBatchId.tree create mode 100644 src/test/sdk/extension/batch-mint-metadata/get-batch-start-id/_getBatchStartId.t.sol create mode 100644 src/test/sdk/extension/batch-mint-metadata/get-batch-start-id/_getBatchStartId.tree create mode 100644 src/test/sdk/extension/batch-mint-metadata/set-base-uri/_setBaseURI.t.sol create mode 100644 src/test/sdk/extension/batch-mint-metadata/set-base-uri/_setBaseURI.tree create mode 100644 src/test/sdk/extension/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.t.sol create mode 100644 src/test/sdk/extension/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.tree create mode 100644 src/test/sdk/extension/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.t.sol create mode 100644 src/test/sdk/extension/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.tree create mode 100644 src/test/sdk/extension/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.t.sol create mode 100644 src/test/sdk/extension/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.tree create mode 100644 src/test/sdk/extension/contract-metadata/set-contract-uri/setContractURI.t.sol create mode 100644 src/test/sdk/extension/contract-metadata/set-contract-uri/setContractURI.tree create mode 100644 src/test/sdk/extension/delayed-reveal/get-reveal-uri/getRevealURI.t.sol create mode 100644 src/test/sdk/extension/delayed-reveal/get-reveal-uri/getRevealURI.tree create mode 100644 src/test/sdk/extension/delayed-reveal/set-encrypted-data/_setEncryptedData.t.sol create mode 100644 src/test/sdk/extension/delayed-reveal/set-encrypted-data/_setEncryptedData.tree create mode 100644 src/test/sdk/extension/drop/claim/claim.t.sol create mode 100644 src/test/sdk/extension/drop/claim/claim.tree create mode 100644 src/test/sdk/extension/drop/get-active-claim-condition-id/getActiveClaimConditionId.t.sol create mode 100644 src/test/sdk/extension/drop/get-active-claim-condition-id/getActiveClaimConditionId.tree create mode 100644 src/test/sdk/extension/drop/set-claim-conditions/setClaimConditions.t.sol create mode 100644 src/test/sdk/extension/drop/set-claim-conditions/setClaimConditions.tree create mode 100644 src/test/sdk/extension/drop/verify-claim/verifyClaim.t.sol create mode 100644 src/test/sdk/extension/drop/verify-claim/verifyClaim.tree create mode 100644 src/test/sdk/extension/lazy-mint/lazy-mint/lazyMint.t.sol create mode 100644 src/test/sdk/extension/lazy-mint/lazy-mint/lazyMint.tree create mode 100644 src/test/sdk/extension/ownable/set-owner/setOwner.t.sol create mode 100644 src/test/sdk/extension/ownable/set-owner/setOwner.tree create mode 100644 src/test/sdk/extension/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol create mode 100644 src/test/sdk/extension/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.tree create mode 100644 src/test/sdk/extension/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol create mode 100644 src/test/sdk/extension/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.tree create mode 100644 src/test/sdk/extension/upgradeable/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.t.sol create mode 100644 src/test/sdk/extension/upgradeable/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.tree create mode 100644 src/test/sdk/extension/upgradeable/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.t.sol create mode 100644 src/test/sdk/extension/upgradeable/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.tree create mode 100644 src/test/sdk/extension/upgradeable/batch-mint-metadata/get-base-uri/_getBaseURI.t.sol create mode 100644 src/test/sdk/extension/upgradeable/batch-mint-metadata/get-base-uri/_getBaseURI.tree create mode 100644 src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-id/_getBatchId.t.sol create mode 100644 src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-id/_getBatchId.tree create mode 100644 src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-start-id/_getBatchStartId.t.sol create mode 100644 src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-start-id/_getBatchStartId.tree create mode 100644 src/test/sdk/extension/upgradeable/batch-mint-metadata/set-base-uri/_setBaseURI.t.sol create mode 100644 src/test/sdk/extension/upgradeable/batch-mint-metadata/set-base-uri/_setBaseURI.tree create mode 100644 src/test/sdk/extension/upgradeable/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.t.sol create mode 100644 src/test/sdk/extension/upgradeable/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.tree create mode 100644 src/test/sdk/extension/upgradeable/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.t.sol create mode 100644 src/test/sdk/extension/upgradeable/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.tree create mode 100644 src/test/sdk/extension/upgradeable/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.t.sol create mode 100644 src/test/sdk/extension/upgradeable/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.tree create mode 100644 src/test/sdk/extension/upgradeable/contract-metadata/set-contract-uri/setContractURI.t.sol create mode 100644 src/test/sdk/extension/upgradeable/contract-metadata/set-contract-uri/setContractURI.tree create mode 100644 src/test/sdk/extension/upgradeable/delayed-reveal/get-reveal-uri/getRevealURI.t.sol create mode 100644 src/test/sdk/extension/upgradeable/delayed-reveal/get-reveal-uri/getRevealURI.tree create mode 100644 src/test/sdk/extension/upgradeable/delayed-reveal/set-encrypted-data/_setEncryptedData.t.sol create mode 100644 src/test/sdk/extension/upgradeable/delayed-reveal/set-encrypted-data/_setEncryptedData.tree create mode 100644 src/test/sdk/extension/upgradeable/drop/claim/claim.t.sol create mode 100644 src/test/sdk/extension/upgradeable/drop/claim/claim.tree create mode 100644 src/test/sdk/extension/upgradeable/drop/get-active-claim-condition-id/getActiveClaimConditionId.t.sol create mode 100644 src/test/sdk/extension/upgradeable/drop/get-active-claim-condition-id/getActiveClaimConditionId.tree create mode 100644 src/test/sdk/extension/upgradeable/drop/set-claim-conditions/setClaimConditions.t.sol create mode 100644 src/test/sdk/extension/upgradeable/drop/set-claim-conditions/setClaimConditions.tree create mode 100644 src/test/sdk/extension/upgradeable/drop/verify-claim/verifyClaim.t.sol create mode 100644 src/test/sdk/extension/upgradeable/drop/verify-claim/verifyClaim.tree create mode 100644 src/test/sdk/extension/upgradeable/lazy-mint/lazy-mint/lazyMint.t.sol create mode 100644 src/test/sdk/extension/upgradeable/lazy-mint/lazy-mint/lazyMint.tree create mode 100644 src/test/sdk/extension/upgradeable/ownable/set-owner/setOwner.t.sol create mode 100644 src/test/sdk/extension/upgradeable/ownable/set-owner/setOwner.tree create mode 100644 src/test/sdk/extension/upgradeable/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol create mode 100644 src/test/sdk/extension/upgradeable/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.tree create mode 100644 src/test/sdk/extension/upgradeable/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol create mode 100644 src/test/sdk/extension/upgradeable/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.tree diff --git a/contracts/extension/BurnToClaim.sol b/contracts/extension/BurnToClaim.sol index 0d1490c56..363a6880f 100644 --- a/contracts/extension/BurnToClaim.sol +++ b/contracts/extension/BurnToClaim.sol @@ -15,6 +15,10 @@ import "./interface/IBurnToClaim.sol"; abstract contract BurnToClaim is IBurnToClaim { BurnToClaimInfo internal burnToClaimInfo; + function getBurnToClaimInfo() public view returns (BurnToClaimInfo memory) { + return burnToClaimInfo; + } + function setBurnToClaimInfo(BurnToClaimInfo calldata _burnToClaimInfo) external virtual { require(_canSetBurnToClaim(), "Not authorized."); require(_burnToClaimInfo.originContractAddress != address(0), "Origin contract not set."); diff --git a/contracts/extension/Drop.sol b/contracts/extension/Drop.sol index 8228c6065..08becaede 100644 --- a/contracts/extension/Drop.sol +++ b/contracts/extension/Drop.sol @@ -124,7 +124,7 @@ abstract contract Drop is IDrop { address _currency, uint256 _pricePerToken, AllowlistProof calldata _allowlistProof - ) public view returns (bool isOverride) { + ) public view virtual returns (bool isOverride) { ClaimCondition memory currentClaimPhase = claimCondition.conditions[_conditionId]; uint256 claimLimit = currentClaimPhase.quantityLimitPerWallet; uint256 claimPrice = currentClaimPhase.pricePerToken; diff --git a/contracts/extension/Drop1155.sol b/contracts/extension/Drop1155.sol index 2d02a86f9..090463b8e 100644 --- a/contracts/extension/Drop1155.sol +++ b/contracts/extension/Drop1155.sol @@ -134,7 +134,7 @@ abstract contract Drop1155 is IDrop1155 { address _currency, uint256 _pricePerToken, AllowlistProof calldata _allowlistProof - ) public view returns (bool isOverride) { + ) public view virtual returns (bool isOverride) { ClaimCondition memory currentClaimPhase = claimCondition[_tokenId].conditions[_conditionId]; uint256 claimLimit = currentClaimPhase.quantityLimitPerWallet; uint256 claimPrice = currentClaimPhase.pricePerToken; diff --git a/contracts/extension/DropSinglePhase.sol b/contracts/extension/DropSinglePhase.sol index cba1f6da6..928bacb35 100644 --- a/contracts/extension/DropSinglePhase.sol +++ b/contracts/extension/DropSinglePhase.sol @@ -100,7 +100,7 @@ abstract contract DropSinglePhase is IDropSinglePhase { address _currency, uint256 _pricePerToken, AllowlistProof calldata _allowlistProof - ) public view returns (bool isOverride) { + ) public view virtual returns (bool isOverride) { ClaimCondition memory currentClaimPhase = claimCondition; uint256 claimLimit = currentClaimPhase.quantityLimitPerWallet; uint256 claimPrice = currentClaimPhase.pricePerToken; diff --git a/contracts/extension/DropSinglePhase1155.sol b/contracts/extension/DropSinglePhase1155.sol index 697013b82..64de27737 100644 --- a/contracts/extension/DropSinglePhase1155.sol +++ b/contracts/extension/DropSinglePhase1155.sol @@ -108,7 +108,7 @@ abstract contract DropSinglePhase1155 is IDropSinglePhase1155 { address _currency, uint256 _pricePerToken, AllowlistProof calldata _allowlistProof - ) public view returns (bool isOverride) { + ) public view virtual returns (bool isOverride) { ClaimCondition memory currentClaimPhase = claimCondition[_tokenId]; uint256 claimLimit = currentClaimPhase.quantityLimitPerWallet; uint256 claimPrice = currentClaimPhase.pricePerToken; diff --git a/contracts/extension/upgradeable/Drop.sol b/contracts/extension/upgradeable/Drop.sol index 4bbfb1525..4be4ff3fe 100644 --- a/contracts/extension/upgradeable/Drop.sol +++ b/contracts/extension/upgradeable/Drop.sol @@ -139,7 +139,7 @@ abstract contract Drop is IDrop { address _currency, uint256 _pricePerToken, AllowlistProof calldata _allowlistProof - ) public view returns (bool isOverride) { + ) public view virtual returns (bool isOverride) { ClaimCondition memory currentClaimPhase = _dropStorage().claimCondition.conditions[_conditionId]; uint256 claimLimit = currentClaimPhase.quantityLimitPerWallet; uint256 claimPrice = currentClaimPhase.pricePerToken; diff --git a/src/test/burn-to-claim-drop-BTT/BurnToClaimDropERC721.t.sol b/src/test/burn-to-claim-drop-BTT/BurnToClaimDropERC721.t.sol new file mode 100644 index 000000000..e3ee1953a --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/BurnToClaimDropERC721.t.sol @@ -0,0 +1,1884 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../utils/BaseTest.sol"; +import { BurnToClaimDropERC721 } from "contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol"; +import { BurnToClaimDrop721Logic, ERC721AUpgradeable, DelayedReveal, LazyMint, Drop, BurnToClaim, PrimarySale, PlatformFee } from "contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Logic.sol"; +import { PermissionsEnumerableImpl } from "contracts/extension/upgradeable/impl/PermissionsEnumerableImpl.sol"; +import { Royalty } from "contracts/extension/upgradeable/Royalty.sol"; +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import { IBurnToClaim } from "contracts/extension/interface/IBurnToClaim.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; +import "contracts/lib/TWStrings.sol"; + +contract BurnToClaimDropERC721Test is BaseTest, IExtension { + using TWStrings for uint256; + using TWStrings for address; + + event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); + event TokenURIRevealed(uint256 indexed index, string revealedURI); + + BurnToClaimDrop721Logic public drop; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address dropImpl = address(new BurnToClaimDropERC721(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + drop = BurnToClaimDrop721Logic( + payable( + address( + new TWProxy( + dropImpl, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ) + ); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](2); + + // Extension: Permissions + address permissions = address(new PermissionsEnumerableImpl()); + + Extension memory extension_permissions; + extension_permissions.metadata = ExtensionMetadata({ + name: "Permissions", + metadataURI: "ipfs://Permissions", + implementation: permissions + }); + + extension_permissions.functions = new ExtensionFunction[](7); + extension_permissions.functions[0] = ExtensionFunction( + Permissions.hasRole.selector, + "hasRole(bytes32,address)" + ); + extension_permissions.functions[1] = ExtensionFunction( + Permissions.hasRoleWithSwitch.selector, + "hasRoleWithSwitch(bytes32,address)" + ); + extension_permissions.functions[2] = ExtensionFunction( + Permissions.grantRole.selector, + "grantRole(bytes32,address)" + ); + extension_permissions.functions[3] = ExtensionFunction( + Permissions.renounceRole.selector, + "renounceRole(bytes32,address)" + ); + extension_permissions.functions[4] = ExtensionFunction( + Permissions.revokeRole.selector, + "revokeRole(bytes32,address)" + ); + extension_permissions.functions[5] = ExtensionFunction( + PermissionsEnumerable.getRoleMemberCount.selector, + "getRoleMemberCount(bytes32)" + ); + extension_permissions.functions[6] = ExtensionFunction( + PermissionsEnumerable.getRoleMember.selector, + "getRoleMember(bytes32,uint256)" + ); + + extensions[0] = extension_permissions; + + address dropLogic = address(new BurnToClaimDrop721Logic()); + + Extension memory extension_drop; + extension_drop.metadata = ExtensionMetadata({ + name: "BurnToClaimDrop721Logic", + metadataURI: "ipfs://BurnToClaimDrop721Logic", + implementation: dropLogic + }); + + extension_drop.functions = new ExtensionFunction[](32); + extension_drop.functions[0] = ExtensionFunction(BurnToClaimDrop721Logic.tokenURI.selector, "tokenURI(uint256)"); + extension_drop.functions[1] = ExtensionFunction( + BurnToClaimDrop721Logic.lazyMint.selector, + "lazyMint(uint256,string,bytes)" + ); + extension_drop.functions[2] = ExtensionFunction( + BurnToClaimDrop721Logic.reveal.selector, + "reveal(uint256,bytes)" + ); + extension_drop.functions[3] = ExtensionFunction(Drop.claimCondition.selector, "claimCondition()"); + extension_drop.functions[4] = ExtensionFunction( + BatchMintMetadata.getBaseURICount.selector, + "getBaseURICount()" + ); + extension_drop.functions[5] = ExtensionFunction( + Drop.claim.selector, + "claim(address,uint256,address,uint256,(bytes32[],uint256,uint256,address),bytes)" + ); + extension_drop.functions[6] = ExtensionFunction( + Drop.setClaimConditions.selector, + "setClaimConditions((uint256,uint256,uint256,uint256,bytes32,uint256,address,string)[],bool)" + ); + extension_drop.functions[7] = ExtensionFunction( + Drop.getActiveClaimConditionId.selector, + "getActiveClaimConditionId()" + ); + extension_drop.functions[8] = ExtensionFunction( + Drop.getClaimConditionById.selector, + "getClaimConditionById(uint256)" + ); + extension_drop.functions[9] = ExtensionFunction( + Drop.getSupplyClaimedByWallet.selector, + "getSupplyClaimedByWallet(uint256,address)" + ); + extension_drop.functions[10] = ExtensionFunction(BurnToClaimDrop721Logic.totalMinted.selector, "totalMinted()"); + extension_drop.functions[11] = ExtensionFunction( + BurnToClaimDrop721Logic.nextTokenIdToMint.selector, + "nextTokenIdToMint()" + ); + extension_drop.functions[12] = ExtensionFunction( + IERC721Upgradeable.setApprovalForAll.selector, + "setApprovalForAll(address,bool)" + ); + extension_drop.functions[13] = ExtensionFunction( + IERC721Upgradeable.approve.selector, + "approve(address,uint256)" + ); + extension_drop.functions[14] = ExtensionFunction( + IERC721Upgradeable.transferFrom.selector, + "transferFrom(address,address,uint256)" + ); + extension_drop.functions[15] = ExtensionFunction(ERC721AUpgradeable.balanceOf.selector, "balanceOf(address)"); + extension_drop.functions[16] = ExtensionFunction( + DelayedReveal.encryptDecrypt.selector, + "encryptDecrypt(bytes,bytes)" + ); + extension_drop.functions[17] = ExtensionFunction( + BurnToClaimDrop721Logic.supportsInterface.selector, + "supportsInterface(bytes4)" + ); + extension_drop.functions[18] = ExtensionFunction(Royalty.royaltyInfo.selector, "royaltyInfo(uint256,uint256)"); + extension_drop.functions[19] = ExtensionFunction( + Royalty.getRoyaltyInfoForToken.selector, + "getRoyaltyInfoForToken(uint256)" + ); + extension_drop.functions[20] = ExtensionFunction( + Royalty.getDefaultRoyaltyInfo.selector, + "getDefaultRoyaltyInfo()" + ); + extension_drop.functions[21] = ExtensionFunction( + Royalty.setDefaultRoyaltyInfo.selector, + "setDefaultRoyaltyInfo(address,uint256)" + ); + extension_drop.functions[22] = ExtensionFunction( + Royalty.setRoyaltyInfoForToken.selector, + "setRoyaltyInfoForToken(uint256,address,uint256)" + ); + extension_drop.functions[23] = ExtensionFunction(IERC721.ownerOf.selector, "ownerOf(uint256)"); + extension_drop.functions[24] = ExtensionFunction(IERC1155.balanceOf.selector, "balanceOf(address,uint256)"); + extension_drop.functions[25] = ExtensionFunction( + BurnToClaim.setBurnToClaimInfo.selector, + "setBurnToClaimInfo((address,uint8,uint256,uint256,address))" + ); + extension_drop.functions[26] = ExtensionFunction( + BurnToClaim.getBurnToClaimInfo.selector, + "getBurnToClaimInfo()" + ); + extension_drop.functions[27] = ExtensionFunction( + BurnToClaim.verifyBurnToClaim.selector, + "verifyBurnToClaim(address,uint256,uint256)" + ); + extension_drop.functions[28] = ExtensionFunction( + BurnToClaimDrop721Logic.burnAndClaim.selector, + "burnAndClaim(uint256,uint256)" + ); + extension_drop.functions[29] = ExtensionFunction( + BurnToClaimDrop721Logic.nextTokenIdToClaim.selector, + "nextTokenIdToClaim()" + ); + extension_drop.functions[30] = ExtensionFunction( + PrimarySale.setPrimarySaleRecipient.selector, + "setPrimarySaleRecipient(address)" + ); + extension_drop.functions[31] = ExtensionFunction( + PlatformFee.setPlatformFeeInfo.selector, + "setPlatformFeeInfo(address,uint256)" + ); + + extensions[1] = extension_drop; + } + + /*/////////////////////////////////////////////////////////////// + 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 ", + TWStrings.toHexString(uint160(caller), 20), + " is missing role ", + TWStrings.toHexString(uint256(role), 32) + ) + ); + + Permissions(address(drop)).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 ", + TWStrings.toHexString(uint160(target), 20), + " is missing role ", + TWStrings.toHexString(uint256(role), 32) + ) + ); + + Permissions(address(drop)).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); + + Permissions(address(drop)).grantRole(role, receiver); + + vm.expectRevert("Can only grant to non holders"); + Permissions(address(drop)).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 = Permissions(address(drop)).hasRole(role, address(0)); + bool checkAdmin = Permissions(address(drop)).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); + Permissions(address(drop)).grantRole(role, receiver); + + // expect revert when granting to a holder + vm.expectRevert("Can only grant to non holders"); + Permissions(address(drop)).grantRole(role, receiver); + + // check if receiver has transfer role + bool checkReceiver = Permissions(address(drop)).hasRole(role, receiver); + assertTrue(checkReceiver); + + // check if role is correctly revoked + Permissions(address(drop)).revokeRole(role, receiver); + checkReceiver = Permissions(address(drop)).hasRole(role, receiver); + assertFalse(checkReceiver); + Permissions(address(drop)).revokeRole(role, address(0)); + checkAddressZero = Permissions(address(drop)).hasRole(role, address(0)); + assertFalse(checkAddressZero); + + vm.stopPrank(); + } + + /** + * @dev Tests contract state for Transfer role. + */ + function test_state_getRoleMember_transferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + + uint256 roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + assertEq(roleMemberCount, 2); + + address roleMember = PermissionsEnumerable(address(drop)).getRoleMember(role, 1); + assertEq(roleMember, address(0)); + + vm.startPrank(deployer); + Permissions(address(drop)).grantRole(role, address(2)); + Permissions(address(drop)).grantRole(role, address(3)); + Permissions(address(drop)).grantRole(role, address(4)); + + roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(address(drop)).getRoleMember(role, i)); + } + console.log(""); + + Permissions(address(drop)).revokeRole(role, address(2)); + roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(address(drop)).getRoleMember(role, i)); + } + console.log(""); + + Permissions(address(drop)).revokeRole(role, address(0)); + roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(address(drop)).getRoleMember(role, i)); + } + console.log(""); + + Permissions(address(drop)).grantRole(role, address(5)); + roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(address(drop)).getRoleMember(role, i)); + } + console.log(""); + + Permissions(address(drop)).grantRole(role, address(0)); + roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(address(drop)).getRoleMember(role, i)); + } + console.log(""); + + Permissions(address(drop)).grantRole(role, address(6)); + roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + console.log(roleMemberCount); + for (uint256 i = 0; i < roleMemberCount; i++) { + console.log(PermissionsEnumerable(address(drop)).getRoleMember(role, i)); + } + console.log(""); + } + + /** + * 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); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, 1, address(0), 0, alp, ""); + + // revoke transfer role from address(0) + vm.prank(deployer); + Permissions(address(drop)).revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + vm.startPrank(receiver); + vm.expectRevert("!Transfer-Role"); + drop.transferFrom(receiver, address(123), 0); + } + + /** + * @dev Tests whether role member count is incremented correctly. + */ + function test_member_count_incremented_properly_when_role_granted() public { + bytes32 role = keccak256("ABC_ROLE"); + address receiver = getActor(0); + + vm.startPrank(deployer); + uint256 roleMemberCount = PermissionsEnumerable(address(drop)).getRoleMemberCount(role); + + assertEq(roleMemberCount, 0); + + Permissions(address(drop)).grantRole(role, receiver); + + assertEq(PermissionsEnumerable(address(drop)).getRoleMemberCount(role), 1); + + vm.stopPrank(); + } + + function test_claimCondition_with_startTimestamp() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].startTimestamp = 100; + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.warp(99); + vm.prank(getActor(5), getActor(5)); + vm.expectRevert("!CONDITION."); + drop.claim(receiver, 1, address(0), 0, alp, ""); + + vm.warp(100); + vm.prank(getActor(4), getActor(4)); + drop.claim(receiver, 1, address(0), 0, alp, ""); + } + + /*/////////////////////////////////////////////////////////////// + Primary sale and Platform fee tests + //////////////////////////////////////////////////////////////*/ + + /// note: Test whether transaction reverts when adding address(0) as primary sale recipient at deploy time + function test_revert_deploy_emptyPrimarySaleRecipient() public { + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address dropImpl = address(new BurnToClaimDropERC721(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + vm.expectRevert("Invalid recipient"); + drop = BurnToClaimDrop721Logic( + payable( + address( + new TWProxy( + dropImpl, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + address(0), + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ) + ); + } + + /// note: Test whether transaction reverts when adding address(0) as primary sale recipient + function test_revert_emptyPrimarySaleRecipient() public { + vm.prank(deployer); + vm.expectRevert("Invalid recipient"); + drop.setPrimarySaleRecipient(address(0)); + } + + /// note: Test whether transaction reverts when adding address(0) as platform fee recipient at deploy time + function test_revert_deploy_emptyPlatformFeeRecipient() public { + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address dropImpl = address(new BurnToClaimDropERC721(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + vm.expectRevert("Invalid recipient"); + drop = BurnToClaimDrop721Logic( + payable( + address( + new TWProxy( + dropImpl, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + address(0) + ) + ) + ) + ) + ) + ); + } + + /// note: Test whether transaction reverts when adding address(0) as platform fee recipient + function test_revert_emptyPlatformFeeRecipient() public { + vm.prank(deployer); + vm.expectRevert("Invalid recipient"); + drop.setPlatformFeeInfo(address(0), 100); + } + + /*/////////////////////////////////////////////////////////////// + Lazy Mint Tests + //////////////////////////////////////////////////////////////*/ + + /* + * note: Testing state changes; lazy mint a batch of tokens with no encrypted base URI. + */ + function test_state_lazyMint_noEncryptedURI() public { + uint256 amountToLazyMint = 100; + string memory baseURI = "ipfs://"; + + uint256 nextTokenIdToMintBefore = drop.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = drop.lazyMint(amountToLazyMint, baseURI, emptyEncodedBytes); + + assertEq(nextTokenIdToMintBefore + amountToLazyMint, drop.nextTokenIdToMint()); + assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); + + for (uint256 i = 0; i < amountToLazyMint; i += 1) { + string memory uri = drop.tokenURI(i); + console.log(uri); + assertEq(uri, string(abi.encodePacked(baseURI, i.toString()))); + } + + vm.stopPrank(); + } + + /* + * note: Testing state changes; lazy mint a batch of tokens with encrypted base URI. + */ + function test_state_lazyMint_withEncryptedURI() public { + uint256 amountToLazyMint = 100; + string memory baseURI = "ipfs://"; + bytes memory encryptedBaseURI = "encryptedBaseURI://"; + bytes32 provenanceHash = bytes32("whatever"); + + uint256 nextTokenIdToMintBefore = drop.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = drop.lazyMint(amountToLazyMint, baseURI, abi.encode(encryptedBaseURI, provenanceHash)); + + assertEq(nextTokenIdToMintBefore + amountToLazyMint, drop.nextTokenIdToMint()); + assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); + + for (uint256 i = 0; i < amountToLazyMint; i += 1) { + string memory uri = drop.tokenURI(i); + console.log(uri); + assertEq(uri, string(abi.encodePacked(baseURI, "0"))); + } + + vm.stopPrank(); + } + + /** + * note: Testing revert condition; an address without MINTER_ROLE calls lazyMint function. + */ + function test_revert_lazyMint_MINTER_ROLE() public { + vm.expectRevert("Not authorized"); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + } + + /* + * note: Testing revert condition; calling tokenURI for invalid batch id. + */ + function test_revert_lazyMint_URIForNonLazyMintedToken() public { + vm.startPrank(deployer); + + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + vm.expectRevert("Invalid tokenId"); + drop.tokenURI(100); + + vm.stopPrank(); + } + + /** + * note: Testing event emission; tokens lazy minted. + */ + function test_event_lazyMint_TokensLazyMinted() public { + vm.startPrank(deployer); + + vm.expectEmit(true, false, false, true); + emit TokensLazyMinted(0, 99, "ipfs://", emptyEncodedBytes); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + vm.stopPrank(); + } + + /* + * note: Fuzz testing state changes; lazy mint a batch of tokens with no encrypted base URI. + */ + function test_fuzz_lazyMint_noEncryptedURI(uint256 x) public { + vm.assume(x > 0); + + uint256 amountToLazyMint = x; + string memory baseURI = "ipfs://"; + + uint256 nextTokenIdToMintBefore = drop.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = drop.lazyMint(amountToLazyMint, baseURI, emptyEncodedBytes); + + assertEq(nextTokenIdToMintBefore + amountToLazyMint, drop.nextTokenIdToMint()); + assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); + + string memory uri = drop.tokenURI(0); + assertEq(uri, string(abi.encodePacked(baseURI, uint256(0).toString()))); + + uri = drop.tokenURI(x - 1); + assertEq(uri, string(abi.encodePacked(baseURI, uint256(x - 1).toString()))); + + /** + * note: this loop takes too long to run with fuzz tests. + */ + // for(uint256 i = 0; i < amountToLazyMint; i += 1) { + // string memory uri = drop.tokenURI(i); + // console.log(uri); + // assertEq(uri, string(abi.encodePacked(baseURI, i.toString()))); + // } + + vm.stopPrank(); + } + + /* + * note: Fuzz testing state changes; lazy mint a batch of tokens with encrypted base URI. + */ + function test_fuzz_lazyMint_withEncryptedURI(uint256 x) public { + vm.assume(x > 0); + + uint256 amountToLazyMint = x; + string memory baseURI = "ipfs://"; + bytes memory encryptedBaseURI = "encryptedBaseURI://"; + bytes32 provenanceHash = bytes32("whatever"); + + uint256 nextTokenIdToMintBefore = drop.nextTokenIdToMint(); + + vm.startPrank(deployer); + uint256 batchId = drop.lazyMint(amountToLazyMint, baseURI, abi.encode(encryptedBaseURI, provenanceHash)); + + assertEq(nextTokenIdToMintBefore + amountToLazyMint, drop.nextTokenIdToMint()); + assertEq(nextTokenIdToMintBefore + amountToLazyMint, batchId); + + string memory uri = drop.tokenURI(0); + assertEq(uri, string(abi.encodePacked(baseURI, "0"))); + + uri = drop.tokenURI(x - 1); + assertEq(uri, string(abi.encodePacked(baseURI, "0"))); + + /** + * note: this loop takes too long to run with fuzz tests. + */ + // for(uint256 i = 0; i < amountToLazyMint; i += 1) { + // string memory uri = drop.tokenURI(1); + // assertEq(uri, string(abi.encodePacked(baseURI, "0"))); + // } + + vm.stopPrank(); + } + + /* + * note: Fuzz testing; a batch of tokens, and nextTokenIdToMint + */ + function test_fuzz_lazyMint_batchMintAndNextTokenIdToMint(uint256 x) public { + vm.assume(x > 0); + vm.startPrank(deployer); + + if (x == 0) { + vm.expectRevert("Zero amount"); + } + drop.lazyMint(x, "ipfs://", emptyEncodedBytes); + + uint256 slot = stdstore.target(address(drop)).sig("nextTokenIdToMint()").find(); + bytes32 loc = bytes32(slot); + uint256 nextTokenIdToMint = uint256(vm.load(address(drop), loc)); + + assertEq(nextTokenIdToMint, x); + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Delayed Reveal Tests + //////////////////////////////////////////////////////////////*/ + + /* + * note: Testing state changes; URI revealed for a batch of tokens. + */ + function test_state_reveal() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + uint256 amountToLazyMint = 100; + bytes memory secretURI = "ipfs://"; + string memory placeholderURI = "abcd://"; + bytes memory encryptedURI = drop.encryptDecrypt(secretURI, key); + bytes32 provenanceHash = keccak256(abi.encodePacked(secretURI, key, block.chainid)); + + drop.lazyMint(amountToLazyMint, placeholderURI, abi.encode(encryptedURI, provenanceHash)); + + for (uint256 i = 0; i < amountToLazyMint; i += 1) { + string memory uri = drop.tokenURI(i); + assertEq(uri, string(abi.encodePacked(placeholderURI, "0"))); + } + + string memory revealedURI = drop.reveal(0, key); + assertEq(revealedURI, string(secretURI)); + + for (uint256 i = 0; i < amountToLazyMint; i += 1) { + string memory uri = drop.tokenURI(i); + assertEq(uri, string(abi.encodePacked(secretURI, i.toString()))); + } + + vm.stopPrank(); + } + + /** + * note: Testing revert condition; an address without MINTER_ROLE calls reveal function. + */ + function test_revert_reveal_MINTER_ROLE() public { + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + vm.prank(deployer); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + + vm.prank(deployer); + drop.reveal(0, "key"); + + vm.expectRevert("not minter."); + drop.reveal(0, "key"); + } + + /* + * note: Testing revert condition; trying to reveal URI for non-existent batch. + */ + function test_revert_reveal_revealingNonExistentBatch() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + drop.reveal(0, "key"); + + console.log(drop.getBaseURICount()); + + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + vm.expectRevert("Invalid index"); + drop.reveal(2, "key"); + + vm.stopPrank(); + } + + /* + * note: Testing revert condition; already revealed URI. + */ + function test_revert_delayedReveal_alreadyRevealed() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + drop.reveal(0, "key"); + + vm.expectRevert("Nothing to reveal"); + drop.reveal(0, "key"); + + vm.stopPrank(); + } + + /* + * note: Testing state changes; revealing URI with an incorrect key. + */ + function testFail_reveal_incorrectKey() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + + string memory revealedURI = drop.reveal(0, "keyy"); + assertEq(revealedURI, "ipfs://"); + + vm.stopPrank(); + } + + /** + * note: Testing event emission; TokenURIRevealed. + */ + function test_event_reveal_TokenURIRevealed() public { + vm.startPrank(deployer); + + bytes memory key = "key"; + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", key); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", key, block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + + vm.expectEmit(true, false, false, true); + emit TokenURIRevealed(0, "ipfs://"); + drop.reveal(0, "key"); + + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Claim Tests + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing revert condition; not enough minted tokens. + */ + function test_revert_claimCondition_notEnoughMintedTokens() public { + vm.warp(1); + + address receiver = getActor(0); + bytes32[] memory proofs = new bytes32[](0); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 200; + + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.expectRevert("!Tokens"); + vm.prank(getActor(6), getActor(6)); + drop.claim(receiver, 101, address(0), 0, alp, ""); + } + + /** + * 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); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 100; + conditions[0].quantityLimitPerWallet = 200; + + vm.prank(deployer); + drop.lazyMint(200, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, 100, address(0), 0, alp, ""); + + vm.expectRevert("!MaxSupply"); + vm.prank(getActor(6), getActor(6)); + drop.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); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + bytes memory errorQty = "!Qty"; + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + drop.claim(receiver, 0, address(0), 0, alp, ""); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + drop.claim(receiver, 101, address(0), 0, alp, ""); + + vm.prank(deployer); + drop.setClaimConditions(conditions, true); + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + drop.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[])); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 0; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0x92Bb439374a091c7507bE100183d8D1Ed2c9dAD3); // in allowlist + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.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); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 100, address(erc20), 0, alp, ""); // claims for free, because allowlist price is 0 + assertEq(drop.getSupplyClaimedByWallet(drop.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[])); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = 5; + alp.currency = address(erc20); + + vm.warp(1); + + address receiver = address(0x92Bb439374a091c7507bE100183d8D1Ed2c9dAD3); // in allowlist + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.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); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(receiver, receiver); + vm.expectRevert("!PriceOrCurrency"); + drop.claim(receiver, 100, address(erc20), 0, alp, ""); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(drop), 10000); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 100, address(erc20), 5, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.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[])); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 300; + alp.pricePerToken = type(uint256).max; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0x92Bb439374a091c7507bE100183d8D1Ed2c9dAD3); // in allowlist + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.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); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(drop), 10000); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 100, address(erc20), 10, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.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[])); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = 0; + alp.pricePerToken = 5; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0x92Bb439374a091c7507bE100183d8D1Ed2c9dAD3); // in allowlist + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.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); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + erc20.mint(receiver, 10000); + vm.prank(receiver); + erc20.approve(address(drop), 10000); + + bytes memory errorQty = "!Qty"; + vm.prank(receiver, receiver); + vm.expectRevert(errorQty); + drop.claim(receiver, 100, address(erc20), 5, alp, ""); // trying to claim more than general limit + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, 10, address(erc20), 5, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.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[])); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = x; + alp.pricePerToken = 0; + alp.currency = address(0); + + vm.warp(1); + + address receiver = address(0x92Bb439374a091c7507bE100183d8D1Ed2c9dAD3); + + // bytes32[] memory proofs = new bytes32[](0); + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = x; + conditions[0].quantityLimitPerWallet = 1; + conditions[0].merkleRoot = root; + + vm.prank(deployer); + drop.lazyMint(2 * x, "ipfs://", emptyEncodedBytes); + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + // vm.prank(getActor(5), getActor(5)); + vm.prank(receiver, receiver); + drop.claim(receiver, x - 5, address(0), 0, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), x - 5); + + bytes memory errorQty = "!Qty"; + + vm.prank(receiver, receiver); + vm.expectRevert(errorQty); + drop.claim(receiver, 6, address(0), 0, alp, ""); + + vm.prank(receiver, receiver); + drop.claim(receiver, 5, address(0), 0, alp, ""); + assertEq(drop.getSupplyClaimedByWallet(drop.getActiveClaimConditionId(), receiver), x); + + vm.prank(receiver, receiver); + vm.expectRevert(errorQty); + drop.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); + + BurnToClaimDrop721Logic.AllowlistProof memory alp; + alp.proof = proofs; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](1); + conditions[0].maxClaimableSupply = 500; + conditions[0].quantityLimitPerWallet = 100; + + vm.prank(deployer); + drop.lazyMint(500, "ipfs://", emptyEncodedBytes); + + vm.prank(deployer); + drop.setClaimConditions(conditions, false); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, 100, address(0), 0, alp, ""); + + bytes memory errorQty = "!Qty"; + + vm.prank(getActor(5), getActor(5)); + vm.expectRevert(errorQty); + drop.claim(receiver, 100, address(0), 0, alp, ""); + + vm.prank(deployer); + drop.setClaimConditions(conditions, true); + + vm.prank(getActor(5), getActor(5)); + drop.claim(receiver, 100, address(0), 0, alp, ""); + } + + /*/////////////////////////////////////////////////////////////// + setClaimConditions + //////////////////////////////////////////////////////////////*/ + + function test_claimCondition_startIdAndCount() public { + vm.startPrank(deployer); + + uint256 currentStartId = 0; + uint256 count = 0; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.ClaimCondition[](2); + conditions[0].startTimestamp = 0; + conditions[0].maxClaimableSupply = 10; + conditions[1].startTimestamp = 1; + conditions[1].maxClaimableSupply = 10; + + drop.setClaimConditions(conditions, false); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 0); + assertEq(count, 2); + + drop.setClaimConditions(conditions, false); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 0); + assertEq(count, 2); + + drop.setClaimConditions(conditions, true); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 2); + assertEq(count, 2); + + drop.setClaimConditions(conditions, true); + (currentStartId, count) = drop.claimCondition(); + assertEq(currentStartId, 4); + assertEq(count, 2); + } + + function test_claimCondition_startPhase() public { + vm.startPrank(deployer); + + uint256 activeConditionId = 0; + + BurnToClaimDrop721Logic.ClaimCondition[] memory conditions = new BurnToClaimDrop721Logic.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; + drop.setClaimConditions(conditions, false); + + vm.expectRevert("!CONDITION."); + drop.getActiveClaimConditionId(); + + vm.warp(10); + activeConditionId = drop.getActiveClaimConditionId(); + assertEq(activeConditionId, 0); + assertEq(drop.getClaimConditionById(activeConditionId).startTimestamp, 10); + assertEq(drop.getClaimConditionById(activeConditionId).maxClaimableSupply, 11); + assertEq(drop.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 12); + + vm.warp(20); + activeConditionId = drop.getActiveClaimConditionId(); + assertEq(activeConditionId, 1); + assertEq(drop.getClaimConditionById(activeConditionId).startTimestamp, 20); + assertEq(drop.getClaimConditionById(activeConditionId).maxClaimableSupply, 21); + assertEq(drop.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 22); + + vm.warp(30); + activeConditionId = drop.getActiveClaimConditionId(); + assertEq(activeConditionId, 2); + assertEq(drop.getClaimConditionById(activeConditionId).startTimestamp, 30); + assertEq(drop.getClaimConditionById(activeConditionId).maxClaimableSupply, 31); + assertEq(drop.getClaimConditionById(activeConditionId).quantityLimitPerWallet, 32); + + vm.warp(40); + assertEq(drop.getActiveClaimConditionId(), 2); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function test_delayedReveal_withNewLazyMintedEmptyBatch() public { + vm.startPrank(deployer); + + bytes memory encryptedURI = drop.encryptDecrypt("ipfs://", "key"); + bytes32 provenanceHash = keccak256(abi.encodePacked("ipfs://", "key", block.chainid)); + drop.lazyMint(100, "", abi.encode(encryptedURI, provenanceHash)); + drop.reveal(0, "key"); + + string memory uri = drop.tokenURI(1); + assertEq(uri, string(abi.encodePacked("ipfs://", "1"))); + + bytes memory newEncryptedURI = drop.encryptDecrypt("ipfs://secret", "key"); + vm.expectRevert("0 amt"); + drop.lazyMint(0, "", abi.encode(newEncryptedURI, provenanceHash)); + + vm.stopPrank(); + } + + /*/////////////////////////////////////////////////////////////// + Burn To Claim + //////////////////////////////////////////////////////////////*/ + + function test_state_burnAndClaim_1155Origin_zeroMintPrice() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc1155); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC1155; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 0; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // check details correctly saved + BurnToClaimDrop721Logic.BurnToClaimInfo memory savedInfo = drop.getBurnToClaimInfo(); + assertEq(savedInfo.originContractAddress, burnToClaimInfo.originContractAddress); + assertTrue(savedInfo.tokenType == burnToClaimInfo.tokenType); + assertEq(savedInfo.tokenId, burnToClaimInfo.tokenId); + assertEq(savedInfo.mintPriceForNewToken, burnToClaimInfo.mintPriceForNewToken); + assertEq(savedInfo.currency, burnToClaimInfo.currency); + + // mint some erc1155 to a claimer + address claimer = getActor(0); + erc1155.mint(claimer, 0, 10); + assertEq(erc1155.balanceOf(claimer, 0), 10); + vm.prank(claimer); + erc1155.setApprovalForAll(address(drop), true); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + drop.burnAndClaim(0, 10); + + // check state + assertEq(erc1155.balanceOf(claimer, 0), 0); + assertEq(drop.balanceOf(claimer), 10); + assertEq(drop.nextTokenIdToClaim(), 10); + } + + function test_state_burnAndClaim_1155Origin_nonZeroMintPrice() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc1155); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC1155; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 1; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // check details correctly saved + BurnToClaimDrop721Logic.BurnToClaimInfo memory savedInfo = drop.getBurnToClaimInfo(); + assertEq(savedInfo.originContractAddress, burnToClaimInfo.originContractAddress); + assertTrue(savedInfo.tokenType == burnToClaimInfo.tokenType); + assertEq(savedInfo.tokenId, burnToClaimInfo.tokenId); + assertEq(savedInfo.mintPriceForNewToken, burnToClaimInfo.mintPriceForNewToken); + assertEq(savedInfo.currency, burnToClaimInfo.currency); + + // mint some erc1155 to a claimer + address claimer = getActor(0); + erc1155.mint(claimer, 0, 10); + assertEq(erc1155.balanceOf(claimer, 0), 10); + vm.prank(claimer); + erc1155.setApprovalForAll(address(drop), true); + + // mint erc20 to claimer, to pay claim price + erc20.mint(claimer, 100); + vm.prank(claimer); + erc20.approve(address(drop), type(uint256).max); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + drop.burnAndClaim(0, 10); + + // check state + assertEq(erc1155.balanceOf(claimer, 0), 0); + assertEq(erc20.balanceOf(claimer), 90); + assertEq(erc20.balanceOf(saleRecipient), 10); + assertEq(drop.balanceOf(claimer), 10); + assertEq(drop.nextTokenIdToClaim(), 10); + } + + function test_state_burnAndClaim_1155Origin_nonZeroMintPrice_nativeToken() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc1155); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC1155; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 1; + burnToClaimInfo.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // check details correctly saved + BurnToClaimDrop721Logic.BurnToClaimInfo memory savedInfo = drop.getBurnToClaimInfo(); + assertEq(savedInfo.originContractAddress, burnToClaimInfo.originContractAddress); + assertTrue(savedInfo.tokenType == burnToClaimInfo.tokenType); + assertEq(savedInfo.tokenId, burnToClaimInfo.tokenId); + assertEq(savedInfo.mintPriceForNewToken, burnToClaimInfo.mintPriceForNewToken); + assertEq(savedInfo.currency, burnToClaimInfo.currency); + + // mint some erc1155 to a claimer + address claimer = getActor(0); + erc1155.mint(claimer, 0, 10); + assertEq(erc1155.balanceOf(claimer, 0), 10); + vm.prank(claimer); + erc1155.setApprovalForAll(address(drop), true); + + // deal ether to claimer, to pay claim price + vm.deal(claimer, 100); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + drop.burnAndClaim{ value: 10 }(0, 10); + + // check state + assertEq(erc1155.balanceOf(claimer, 0), 0); + assertEq(claimer.balance, 90); + assertEq(saleRecipient.balance, 10); + assertEq(drop.balanceOf(claimer), 10); + assertEq(drop.nextTokenIdToClaim(), 10); + } + + function test_state_burnAndClaim_721Origin_zeroMintPrice() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc721); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC721; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 0; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // check details correctly saved + BurnToClaimDrop721Logic.BurnToClaimInfo memory savedInfo = drop.getBurnToClaimInfo(); + assertEq(savedInfo.originContractAddress, burnToClaimInfo.originContractAddress); + assertTrue(savedInfo.tokenType == burnToClaimInfo.tokenType); + assertEq(savedInfo.tokenId, burnToClaimInfo.tokenId); + assertEq(savedInfo.mintPriceForNewToken, burnToClaimInfo.mintPriceForNewToken); + assertEq(savedInfo.currency, burnToClaimInfo.currency); + + // mint some erc721 to a claimer + address claimer = getActor(0); + erc721.mint(claimer, 10); + assertEq(erc721.balanceOf(claimer), 10); + vm.prank(claimer); + erc721.setApprovalForAll(address(drop), true); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + drop.burnAndClaim(0, 1); + + // check state + assertEq(erc721.balanceOf(claimer), 9); + assertEq(drop.balanceOf(claimer), 1); + assertEq(drop.nextTokenIdToClaim(), 1); + + vm.expectRevert("ERC721: invalid token ID"); // because the token doesn't exist anymore + erc721.ownerOf(0); + } + + function test_state_burnAndClaim_721Origin_nonZeroMintPrice() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc721); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC721; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 1; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // check details correctly saved + BurnToClaimDrop721Logic.BurnToClaimInfo memory savedInfo = drop.getBurnToClaimInfo(); + assertEq(savedInfo.originContractAddress, burnToClaimInfo.originContractAddress); + assertTrue(savedInfo.tokenType == burnToClaimInfo.tokenType); + assertEq(savedInfo.tokenId, burnToClaimInfo.tokenId); + assertEq(savedInfo.mintPriceForNewToken, burnToClaimInfo.mintPriceForNewToken); + assertEq(savedInfo.currency, burnToClaimInfo.currency); + + // mint some erc721 to a claimer + address claimer = getActor(0); + erc721.mint(claimer, 10); + assertEq(erc721.balanceOf(claimer), 10); + vm.prank(claimer); + erc721.setApprovalForAll(address(drop), true); + + // mint erc20 to claimer, to pay claim price + erc20.mint(claimer, 100); + vm.prank(claimer); + erc20.approve(address(drop), type(uint256).max); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + drop.burnAndClaim(0, 1); + + // check state + assertEq(erc721.balanceOf(claimer), 9); + assertEq(drop.balanceOf(claimer), 1); + assertEq(drop.nextTokenIdToClaim(), 1); + assertEq(erc20.balanceOf(claimer), 99); + assertEq(erc20.balanceOf(saleRecipient), 1); + + vm.expectRevert("ERC721: invalid token ID"); // because the token doesn't exist anymore + erc721.ownerOf(0); + } + + function test_state_burnAndClaim_721Origin_nonZeroMintPrice_nativeToken() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc721); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC721; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 1; + burnToClaimInfo.currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // check details correctly saved + BurnToClaimDrop721Logic.BurnToClaimInfo memory savedInfo = drop.getBurnToClaimInfo(); + assertEq(savedInfo.originContractAddress, burnToClaimInfo.originContractAddress); + assertTrue(savedInfo.tokenType == burnToClaimInfo.tokenType); + assertEq(savedInfo.tokenId, burnToClaimInfo.tokenId); + assertEq(savedInfo.mintPriceForNewToken, burnToClaimInfo.mintPriceForNewToken); + assertEq(savedInfo.currency, burnToClaimInfo.currency); + + // mint some erc721 to a claimer + address claimer = getActor(0); + erc721.mint(claimer, 10); + assertEq(erc721.balanceOf(claimer), 10); + vm.prank(claimer); + erc721.setApprovalForAll(address(drop), true); + + // deal ether to claimer, to pay claim price + vm.deal(claimer, 100); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + drop.burnAndClaim{ value: 1 }(0, 1); + + // check state + assertEq(erc721.balanceOf(claimer), 9); + assertEq(drop.balanceOf(claimer), 1); + assertEq(drop.nextTokenIdToClaim(), 1); + assertEq(claimer.balance, 99); + assertEq(saleRecipient.balance, 1); + + vm.expectRevert("ERC721: invalid token ID"); // because the token doesn't exist anymore + erc721.ownerOf(0); + } + + function test_revert_burnAndClaim_originNotSet() public { + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.expectRevert(); + drop.burnAndClaim(0, 1); + } + + function test_revert_burnAndClaim_noLazyMintedTokens() public { + // burn and claim + vm.expectRevert("!Tokens"); + drop.burnAndClaim(0, 1); + } + + function test_revert_burnAndClaim_invalidTokenId() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc1155); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC1155; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 0; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // mint some erc1155 to a claimer + address claimer = getActor(0); + erc1155.mint(claimer, 0, 10); + assertEq(erc1155.balanceOf(claimer, 0), 10); + vm.prank(claimer); + erc1155.setApprovalForAll(address(drop), true); + + // burn and claim + vm.prank(claimer); + vm.expectRevert("Invalid token Id"); + drop.burnAndClaim(1, 1); + } + + function test_revert_burnAndClaim_notEnoughBalance() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc1155); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC1155; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 0; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // mint some erc1155 to a claimer + address claimer = getActor(0); + erc1155.mint(claimer, 0, 10); + assertEq(erc1155.balanceOf(claimer, 0), 10); + vm.prank(claimer); + erc1155.setApprovalForAll(address(drop), true); + + // burn and claim + vm.prank(claimer); + vm.expectRevert("!Balance"); + drop.burnAndClaim(0, 11); + } + + function test_revert_burnAndClaim_notOwnerOfToken() public { + IBurnToClaim.BurnToClaimInfo memory burnToClaimInfo; + + burnToClaimInfo.originContractAddress = address(erc721); + burnToClaimInfo.tokenType = IBurnToClaim.TokenType.ERC721; + burnToClaimInfo.tokenId = 0; + burnToClaimInfo.mintPriceForNewToken = 1; + burnToClaimInfo.currency = address(erc20); + + // set origin contract details for burn and claim + vm.prank(deployer); + drop.setBurnToClaimInfo(burnToClaimInfo); + + // mint some erc721 to a claimer + address claimer = getActor(0); + erc721.mint(claimer, 10); + assertEq(erc721.balanceOf(claimer), 10); + vm.prank(claimer); + erc721.setApprovalForAll(address(drop), true); + + // mint erc721 to another address + erc721.mint(address(0x567), 5); + + // lazy mint tokens + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", emptyEncodedBytes); + + // burn and claim + vm.prank(claimer); + vm.expectRevert("!Owner"); + drop.burnAndClaim(11, 1); + } + + /*/////////////////////////////////////////////////////////////// + Extension Role and Upgradeability + //////////////////////////////////////////////////////////////*/ + + // function test_addExtension() public { + // address permissionsNew = address(new PermissionsEnumerableImpl()); + + // Extension memory extension_permissions_new; + // extension_permissions_new.metadata = ExtensionMetadata({ + // name: "PermissionsNew", + // metadataURI: "ipfs://PermissionsNew", + // implementation: permissionsNew + // }); + + // extension_permissions_new.functions = new ExtensionFunction[](4); + // extension_permissions_new.functions[0] = ExtensionFunction( + // Permissions.hasRole.selector, + // "hasRole(bytes32,address)" + // ); + // extension_permissions_new.functions[1] = ExtensionFunction( + // Permissions.hasRoleWithSwitch.selector, + // "hasRoleWithSwitch(bytes32,address)" + // ); + // extension_permissions_new.functions[2] = ExtensionFunction( + // Permissions.grantRole.selector, + // "grantRole(bytes32,address)" + // ); + // extension_permissions_new.functions[3] = ExtensionFunction( + // PermissionsEnumerable.getRoleMemberCount.selector, + // "getRoleMemberCount(bytes32)" + // ); + + // // cast drop to router type + // BurnToClaimDropERC721 dropRouter = BurnToClaimDropERC721(payable(address(drop))); + + // vm.prank(deployer); + // dropRouter.addExtension(extension_permissions_new); + + // // assertEq( + // // dropRouter.getExtensionForFunction(PermissionsEnumerable.getRoleMemberCount.selector).name, + // // "PermissionsNew" + // // ); + + // // assertEq( + // // dropRouter.getExtensionForFunction(PermissionsEnumerable.getRoleMemberCount.selector).implementation, + // // permissionsNew + // // ); + // } + + function test_revert_addExtension_NotAuthorized() public { + Extension memory extension_permissions_new; + + // cast drop to router type + BurnToClaimDropERC721 dropRouter = BurnToClaimDropERC721(payable(address(drop))); + + vm.prank(address(0x123)); + vm.expectRevert("ExtensionManager: unauthorized."); + dropRouter.addExtension(extension_permissions_new); + } + + function test_revert_addExtension_deployerRenounceExtensionRole() public { + Extension memory extension_permissions_new; + + // cast drop to router type + BurnToClaimDropERC721 dropRouter = BurnToClaimDropERC721(payable(address(drop))); + + vm.prank(deployer); + Permissions(address(drop)).renounceRole(keccak256("EXTENSION_ROLE"), deployer); + + vm.prank(deployer); + vm.expectRevert("ExtensionManager: unauthorized."); + dropRouter.addExtension(extension_permissions_new); + + vm.startPrank(deployer); + vm.expectRevert( + abi.encodePacked( + "Permissions: account ", + TWStrings.toHexString(uint160(deployer), 20), + " is missing role ", + TWStrings.toHexString(uint256(keccak256("EXTENSION_ROLE")), 32) + ) + ); + Permissions(address(drop)).grantRole(keccak256("EXTENSION_ROLE"), address(0x12345)); + vm.stopPrank(); + } +} diff --git a/src/test/burn-to-claim-drop-BTT/logic/burn-and-claim/burnAndClaim.t.sol b/src/test/burn-to-claim-drop-BTT/logic/burn-and-claim/burnAndClaim.t.sol new file mode 100644 index 000000000..3f3ef7600 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/logic/burn-and-claim/burnAndClaim.t.sol @@ -0,0 +1,802 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import { BurnToClaimDropERC721 } from "contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol"; +import { BurnToClaimDrop721Logic, ERC721AUpgradeable, DelayedReveal, LazyMint, Drop, BurnToClaim, PrimarySale, PlatformFee } from "contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Logic.sol"; +import { PermissionsEnumerableImpl } from "contracts/extension/upgradeable/impl/PermissionsEnumerableImpl.sol"; +import { Royalty } from "contracts/extension/upgradeable/Royalty.sol"; +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import { IBurnToClaim } from "contracts/extension/interface/IBurnToClaim.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; +import "contracts/lib/TWStrings.sol"; + +contract BurnToClaimDropERC721Logic_BurnAndClaim is BaseTest, IExtension { + using TWStrings for uint256; + using TWStrings for address; + + event TokensBurnedAndClaimed( + address indexed originContract, + address indexed tokenOwner, + uint256 indexed burnTokenId, + uint256 quantity + ); + + BurnToClaimDrop721Logic public drop; + uint256 internal _tokenId; + uint256 internal _quantity; + uint256 internal _msgValue; + uint256[] internal batchIds; + address internal caller; + bytes internal data; + IBurnToClaim.BurnToClaimInfo internal info; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address dropImpl = address(new BurnToClaimDropERC721(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + drop = BurnToClaimDrop721Logic( + payable( + address( + new TWProxy( + dropImpl, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ) + ); + + caller = getActor(5); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + erc20.mint(caller, 1_000 ether); + vm.deal(caller, 1_000 ether); + + erc721.mint(deployer, 100); + erc721NonBurnable.mint(deployer, 100); + + erc1155NonBurnable.mint(deployer, 0, 100); + erc1155.mint(deployer, 0, 100); + erc1155.mint(deployer, 1, 100); + + vm.startPrank(deployer); + erc721.setApprovalForAll(address(drop), true); + erc1155.setApprovalForAll(address(drop), true); + erc20.approve(address(drop), type(uint256).max); + vm.stopPrank(); + + vm.startPrank(caller); + erc721.setApprovalForAll(address(drop), true); + erc1155.setApprovalForAll(address(drop), true); + erc20.approve(address(drop), type(uint256).max); + vm.stopPrank(); + + // startId = 0; + // mint 5 batches + // vm.startPrank(deployer); + // for (uint256 i = 0; i < 5; i++) { + // uint256 _amount = (i + 1) * 10; + // uint256 batchId = startId + _amount; + // batchIds.push(batchId); + + // string memory baseURI = Strings.toString(batchId); + // startId = drop.lazyMint(_amount, baseURI, ""); + // } + // vm.stopPrank(); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](2); + + // Extension: Permissions + address permissions = address(new PermissionsEnumerableImpl()); + + Extension memory extension_permissions; + extension_permissions.metadata = ExtensionMetadata({ + name: "Permissions", + metadataURI: "ipfs://Permissions", + implementation: permissions + }); + + extension_permissions.functions = new ExtensionFunction[](1); + extension_permissions.functions[0] = ExtensionFunction( + Permissions.hasRole.selector, + "hasRole(bytes32,address)" + ); + + extensions[0] = extension_permissions; + + address dropLogic = address(new BurnToClaimDrop721Logic()); + + Extension memory extension_drop; + extension_drop.metadata = ExtensionMetadata({ + name: "BurnToClaimDrop721Logic", + metadataURI: "ipfs://BurnToClaimDrop721Logic", + implementation: dropLogic + }); + + extension_drop.functions = new ExtensionFunction[](10); + extension_drop.functions[0] = ExtensionFunction(BurnToClaimDrop721Logic.tokenURI.selector, "tokenURI(uint256)"); + extension_drop.functions[1] = ExtensionFunction( + BurnToClaimDrop721Logic.lazyMint.selector, + "lazyMint(uint256,string,bytes)" + ); + extension_drop.functions[2] = ExtensionFunction( + BurnToClaimDrop721Logic.setMaxTotalMinted.selector, + "setMaxTotalMinted(uint256)" + ); + extension_drop.functions[3] = ExtensionFunction( + BurnToClaimDrop721Logic.nextTokenIdToMint.selector, + "nextTokenIdToMint()" + ); + extension_drop.functions[4] = ExtensionFunction( + BurnToClaimDrop721Logic.burnAndClaim.selector, + "burnAndClaim(uint256,uint256)" + ); + extension_drop.functions[5] = ExtensionFunction( + BurnToClaim.getBurnToClaimInfo.selector, + "getBurnToClaimInfo()" + ); + extension_drop.functions[6] = ExtensionFunction( + BurnToClaim.setBurnToClaimInfo.selector, + "setBurnToClaimInfo((address,uint8,uint256,uint256,address))" + ); + extension_drop.functions[7] = ExtensionFunction( + BurnToClaimDrop721Logic.nextTokenIdToClaim.selector, + "nextTokenIdToClaim()" + ); + extension_drop.functions[8] = ExtensionFunction(ERC721AUpgradeable.balanceOf.selector, "balanceOf(address)"); + extension_drop.functions[9] = ExtensionFunction(ERC721AUpgradeable.ownerOf.selector, "ownerOf(uint256)"); + + extensions[1] = extension_drop; + } + + function test_burnAndClaim_notEnoughLazyMintedTokens() public { + vm.expectRevert("!Tokens"); + drop.burnAndClaim(0, 1); + } + + modifier whenEnoughLazyMintedTokens() { + vm.prank(deployer); + drop.lazyMint(1000, "ipfs://", ""); + _; + } + + function test_burnAndClaim_exceedMaxTotalMint() public whenEnoughLazyMintedTokens { + vm.prank(deployer); + drop.setMaxTotalMinted(1); //set max total mint cap as 1 + + vm.expectRevert("exceed max total mint cap."); + drop.burnAndClaim(0, 2); + } + + modifier whenNotExceedMaxTotalMinted() { + vm.prank(deployer); + drop.setMaxTotalMinted(1000); + _; + } + + function test_burnAndClaim_burnToClaimInfoNotSet() public whenEnoughLazyMintedTokens whenNotExceedMaxTotalMinted { + // it will fail when verifyClaim tries to check owner/balance on nft contract which is still address(0) + vm.expectRevert(); + drop.burnAndClaim(0, 1); + } + + // ================== + // ======= Test branch: burn-to-claim origin contract is ERC721 + // ================== + + modifier whenBurnToClaimInfoSetERC721() { + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721NonBurnable), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }); + + vm.prank(deployer); + drop.setBurnToClaimInfo(info); + + _; + } + + function test_burnAndClaim_ERC721_invalidQuantity() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSetERC721 + { + vm.expectRevert("Invalid amount"); + drop.burnAndClaim(0, 0); + } + + modifier whenValidQuantityERC721() { + _quantity = 1; + _; + } + + function test_burnAndClaim_ERC721_notOwner() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSetERC721 + whenValidQuantityERC721 + { + vm.expectRevert("!Owner"); + drop.burnAndClaim(_tokenId, _quantity); + } + + modifier whenCorrectOwnerERC721() { + vm.startPrank(deployer); + erc721NonBurnable.transferFrom(deployer, caller, _tokenId); + erc721.transferFrom(deployer, caller, _tokenId); + vm.stopPrank(); + _; + } + + function test_burnAndClaim_ERC721_notBurnable() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSetERC721 + whenValidQuantityERC721 + whenCorrectOwnerERC721 + { + vm.expectRevert(); // `EvmError: Revert` when trying to burn on a non-burnable contract + vm.prank(caller); + drop.burnAndClaim(_tokenId, _quantity); + } + + modifier whenBurnToClaimInfoSet_ERC721Burnable() { + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }); + + vm.prank(deployer); + drop.setBurnToClaimInfo(info); + + _; + } + + function test_burnAndClaim_ERC721_mintPriceZero_msgValueNonZero() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC721Burnable + whenValidQuantityERC721 + whenCorrectOwnerERC721 + { + vm.expectRevert("!Value"); + vm.prank(caller); + drop.burnAndClaim{ value: 1 }(_tokenId, _quantity); + } + + modifier whenMsgValueZero() { + _msgValue = 0; + _; + } + + function test_burnAndClaim_ERC721_mintPriceZero() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC721Burnable + whenValidQuantityERC721 + whenCorrectOwnerERC721 + whenMsgValueZero + { + // state before + uint256 _nextTokenIdToClaim = drop.nextTokenIdToClaim(); + + // burn and claim + vm.prank(caller); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + + // check state after + vm.expectRevert(); // because token non-existent after burning + erc721.ownerOf(_tokenId); + + assertEq(drop.balanceOf(caller), _quantity); + assertEq(drop.ownerOf(_nextTokenIdToClaim), caller); + assertEq(drop.nextTokenIdToClaim(), _nextTokenIdToClaim + _quantity); + } + + function test_burnAndClaim_ERC721_mintPriceZero_event() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC721Burnable + whenValidQuantityERC721 + whenCorrectOwnerERC721 + whenMsgValueZero + { + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensBurnedAndClaimed(address(erc721), caller, _tokenId, _quantity); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + } + + modifier whenBurnToClaimInfoSet_ERC721Burnable_nonZeroPriceNativeToken() { + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 100, + currency: NATIVE_TOKEN + }); + + vm.prank(deployer); + drop.setBurnToClaimInfo(info); + + _; + } + + function test_burnAndClaim_ERC721_mintPriceNonZero_nativeToken_incorrectMsgValue() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC721Burnable_nonZeroPriceNativeToken + whenValidQuantityERC721 + whenCorrectOwnerERC721 + { + uint256 incorrectTotalPrice = (info.mintPriceForNewToken * _quantity) + 1; + + vm.prank(caller); + vm.expectRevert("Invalid msg value"); + drop.burnAndClaim{ value: incorrectTotalPrice }(_tokenId, _quantity); + } + + modifier whenCorrectMsgValue() { + _msgValue = info.mintPriceForNewToken * _quantity; + _; + } + + function test_burnAndClaim_ERC721_mintPriceNonZero_nativeToken() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC721Burnable_nonZeroPriceNativeToken + whenValidQuantityERC721 + whenCorrectOwnerERC721 + whenCorrectMsgValue + { + // state before + uint256 _nextTokenIdToClaim = drop.nextTokenIdToClaim(); + assertEq(platformFeeRecipient.balance, 0); + assertEq(saleRecipient.balance, 0); + assertEq(caller.balance, 1000 ether); + + // burn and claim + vm.prank(caller); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + + // check state after + uint256 totalPrice = (info.mintPriceForNewToken * _quantity); + uint256 _platformFee = (totalPrice * platformFeeBps) / 10_000; + uint256 _saleProceeds = totalPrice - _platformFee; + vm.expectRevert(); // because token non-existent after burning + erc721.ownerOf(_tokenId); + + assertEq(drop.balanceOf(caller), _quantity); + assertEq(drop.ownerOf(_nextTokenIdToClaim), caller); + assertEq(drop.nextTokenIdToClaim(), _nextTokenIdToClaim + _quantity); + assertEq(platformFeeRecipient.balance, _platformFee); + assertEq(saleRecipient.balance, _saleProceeds); + assertEq(caller.balance, 1000 ether - totalPrice); + } + + function test_burnAndClaim_ERC721_mintPriceNonZero_nativeToken_event() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC721Burnable_nonZeroPriceNativeToken + whenValidQuantityERC721 + whenCorrectOwnerERC721 + whenCorrectMsgValue + { + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensBurnedAndClaimed(address(erc721), caller, _tokenId, _quantity); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + } + + modifier whenBurnToClaimInfoSet_ERC721Burnable_nonZeroPriceERC20() { + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 100, + currency: address(erc20) + }); + + vm.prank(deployer); + drop.setBurnToClaimInfo(info); + + _; + } + + function test_burnAndClaim_ERC721_mintPriceNonZero_ERC20_nonZeroMsgValue() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC721Burnable_nonZeroPriceERC20 + whenValidQuantityERC721 + whenCorrectOwnerERC721 + { + vm.prank(caller); + vm.expectRevert("Invalid msg value"); + drop.burnAndClaim{ value: 1 }(_tokenId, _quantity); + } + + function test_burnAndClaim_ERC721_mintPriceNonZero_ERC20() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC721Burnable_nonZeroPriceERC20 + whenValidQuantityERC721 + whenCorrectOwnerERC721 + whenMsgValueZero + { + // state before + uint256 _nextTokenIdToClaim = drop.nextTokenIdToClaim(); + assertEq(erc20.balanceOf(platformFeeRecipient), 0); + assertEq(erc20.balanceOf(saleRecipient), 0); + assertEq(erc20.balanceOf(caller), 1000 ether); + + // burn and claim + vm.prank(caller); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + + // check state after + uint256 totalPrice = (info.mintPriceForNewToken * _quantity); + uint256 _platformFee = (totalPrice * platformFeeBps) / 10_000; + uint256 _saleProceeds = totalPrice - _platformFee; + vm.expectRevert(); // because token non-existent after burning + erc721.ownerOf(_tokenId); + + assertEq(drop.balanceOf(caller), _quantity); + assertEq(drop.ownerOf(_nextTokenIdToClaim), caller); + assertEq(drop.nextTokenIdToClaim(), _nextTokenIdToClaim + _quantity); + assertEq(erc20.balanceOf(platformFeeRecipient), _platformFee); + assertEq(erc20.balanceOf(saleRecipient), _saleProceeds); + assertEq(erc20.balanceOf(caller), 1000 ether - totalPrice); + } + + function test_burnAndClaim_ERC721_mintPriceNonZero_ERC20_event() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC721Burnable_nonZeroPriceERC20 + whenValidQuantityERC721 + whenCorrectOwnerERC721 + whenMsgValueZero + { + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensBurnedAndClaimed(address(erc721), caller, _tokenId, _quantity); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + } + + // ================== + // ======= Test branch: burn-to-claim origin contract is ERC1155 + // ================== + + modifier whenBurnToClaimInfoSetERC1155() { + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155NonBurnable), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }); + + vm.prank(deployer); + drop.setBurnToClaimInfo(info); + + _; + } + + function test_burnAndClaim_ERC1155_invalidTokenId() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSetERC1155 + { + vm.expectRevert("Invalid token Id"); + drop.burnAndClaim(1, 1); + } + + modifier whenValidTokenIdERC1155() { + _quantity = 1; + _tokenId = 0; + _; + } + + function test_burnAndClaim_ERC1155_notEnoughBalance() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSetERC1155 + whenValidTokenIdERC1155 + { + vm.expectRevert("!Balance"); + vm.prank(caller); + drop.burnAndClaim(_tokenId, _quantity); + } + + modifier whenEnoughBalanceERC1155() { + vm.startPrank(deployer); + erc1155NonBurnable.safeTransferFrom(deployer, caller, _tokenId, 100, ""); + erc1155.safeTransferFrom(deployer, caller, _tokenId, 100, ""); + vm.stopPrank(); + _; + } + + function test_burnAndClaim_ERC1155_notBurnable() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSetERC1155 + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + { + vm.expectRevert(); // `EvmError: Revert` when trying to burn on a non-burnable contract + vm.prank(caller); + drop.burnAndClaim(_tokenId, _quantity); + } + + modifier whenBurnToClaimInfoSet_ERC1155Burnable() { + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }); + + vm.prank(deployer); + drop.setBurnToClaimInfo(info); + + _; + } + + function test_burnAndClaim_ERC1155_mintPriceZero_msgValueNonZero() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC1155Burnable + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + { + vm.expectRevert("!Value"); + vm.prank(caller); + drop.burnAndClaim{ value: 1 }(_tokenId, _quantity); + } + + function test_burnAndClaim_ERC1155_mintPriceZero() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC1155Burnable + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + whenMsgValueZero + { + // state before + uint256 _nextTokenIdToClaim = drop.nextTokenIdToClaim(); + + // burn and claim + vm.prank(caller); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + + // check state after + assertEq(erc1155.balanceOf(caller, _tokenId), 100 - _quantity); + assertEq(drop.balanceOf(caller), _quantity); + assertEq(drop.ownerOf(_nextTokenIdToClaim), caller); + assertEq(drop.nextTokenIdToClaim(), _nextTokenIdToClaim + _quantity); + } + + function test_burnAndClaim_ERC1155_mintPriceZero_event() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC1155Burnable + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + whenMsgValueZero + { + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensBurnedAndClaimed(address(erc1155), caller, _tokenId, _quantity); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + } + + modifier whenBurnToClaimInfoSet_ERC1155Burnable_nonZeroPriceNativeToken() { + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 0, + mintPriceForNewToken: 100, + currency: NATIVE_TOKEN + }); + + vm.prank(deployer); + drop.setBurnToClaimInfo(info); + + _; + } + + function test_burnAndClaim_ERC1155_mintPriceNonZero_nativeToken_incorrectMsgValue() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC1155Burnable_nonZeroPriceNativeToken + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + { + uint256 incorrectTotalPrice = (info.mintPriceForNewToken * _quantity) + 1; + + vm.prank(caller); + vm.expectRevert("Invalid msg value"); + drop.burnAndClaim{ value: incorrectTotalPrice }(_tokenId, _quantity); + } + + function test_burnAndClaim_ERC1155_mintPriceNonZero_nativeToken() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC1155Burnable_nonZeroPriceNativeToken + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + whenCorrectMsgValue + { + // state before + uint256 _nextTokenIdToClaim = drop.nextTokenIdToClaim(); + assertEq(platformFeeRecipient.balance, 0); + assertEq(saleRecipient.balance, 0); + assertEq(caller.balance, 1000 ether); + + // burn and claim + vm.prank(caller); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + + // check state after + uint256 totalPrice = (info.mintPriceForNewToken * _quantity); + uint256 _platformFee = (totalPrice * platformFeeBps) / 10_000; + uint256 _saleProceeds = totalPrice - _platformFee; + + assertEq(erc1155.balanceOf(caller, _tokenId), 100 - _quantity); + assertEq(drop.balanceOf(caller), _quantity); + assertEq(drop.ownerOf(_nextTokenIdToClaim), caller); + assertEq(drop.nextTokenIdToClaim(), _nextTokenIdToClaim + _quantity); + assertEq(platformFeeRecipient.balance, _platformFee); + assertEq(saleRecipient.balance, _saleProceeds); + assertEq(caller.balance, 1000 ether - totalPrice); + } + + function test_burnAndClaim_ERC1155_mintPriceNonZero_nativeToken_event() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC1155Burnable_nonZeroPriceNativeToken + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + whenCorrectMsgValue + { + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensBurnedAndClaimed(address(erc1155), caller, _tokenId, _quantity); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + } + + modifier whenBurnToClaimInfoSet_ERC1155Burnable_nonZeroPriceERC20() { + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 0, + mintPriceForNewToken: 100, + currency: address(erc20) + }); + + vm.prank(deployer); + drop.setBurnToClaimInfo(info); + + _; + } + + function test_burnAndClaim_ERC1155_mintPriceNonZero_ERC20_nonZeroMsgValue() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC1155Burnable_nonZeroPriceERC20 + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + { + vm.prank(caller); + vm.expectRevert("Invalid msg value"); + drop.burnAndClaim{ value: 1 }(_tokenId, _quantity); + } + + function test_burnAndClaim_ERC1155_mintPriceNonZero_ERC20() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC1155Burnable_nonZeroPriceERC20 + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + whenMsgValueZero + { + // state before + uint256 _nextTokenIdToClaim = drop.nextTokenIdToClaim(); + assertEq(erc20.balanceOf(platformFeeRecipient), 0); + assertEq(erc20.balanceOf(saleRecipient), 0); + assertEq(erc20.balanceOf(caller), 1000 ether); + + // burn and claim + vm.prank(caller); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + + // check state after + uint256 totalPrice = (info.mintPriceForNewToken * _quantity); + uint256 _platformFee = (totalPrice * platformFeeBps) / 10_000; + uint256 _saleProceeds = totalPrice - _platformFee; + + assertEq(erc1155.balanceOf(caller, _tokenId), 100 - _quantity); + assertEq(drop.balanceOf(caller), _quantity); + assertEq(drop.ownerOf(_nextTokenIdToClaim), caller); + assertEq(drop.nextTokenIdToClaim(), _nextTokenIdToClaim + _quantity); + assertEq(erc20.balanceOf(platformFeeRecipient), _platformFee); + assertEq(erc20.balanceOf(saleRecipient), _saleProceeds); + assertEq(erc20.balanceOf(caller), 1000 ether - totalPrice); + } + + function test_burnAndClaim_ERC1155_mintPriceNonZero_ERC20_event() + public + whenEnoughLazyMintedTokens + whenNotExceedMaxTotalMinted + whenBurnToClaimInfoSet_ERC1155Burnable_nonZeroPriceERC20 + whenValidTokenIdERC1155 + whenEnoughBalanceERC1155 + whenMsgValueZero + { + vm.prank(caller); + vm.expectEmit(true, true, true, true); + emit TokensBurnedAndClaimed(address(erc1155), caller, _tokenId, _quantity); + drop.burnAndClaim{ value: _msgValue }(_tokenId, _quantity); + } +} diff --git a/src/test/burn-to-claim-drop-BTT/logic/burn-and-claim/burnAndClaim.tree b/src/test/burn-to-claim-drop-BTT/logic/burn-and-claim/burnAndClaim.tree new file mode 100644 index 000000000..00e6af1b9 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/logic/burn-and-claim/burnAndClaim.tree @@ -0,0 +1,83 @@ +burnAndClaim(uint256 _burnTokenId, uint256 _quantity) +├── when the sum of `_quantity` and total minted is greater than nextTokenIdToLazyMint +│ └── it should revert ✅ +└── when the sum of `_quantity` and total minted less than or equal to nextTokenIdToLazyMint + └── when maxTotalMinted is not zero ✅ // TODO when zero + └── when the sum of `_quantity` and total minted greater than maxTotalMinted + │ └── it should revert ✅ + └── when the sum of `_quantity` and total minted less than or equal to maxTotalMinted + ├── when burn-to-claim info is not set + │ └── it should revert ✅ + └── when burn-to-claim info is set, with token type ERC721 + │ ├── when `_quantity` is not 1 + │ │ └── it should revert ✅ + │ └── when `_quantity` param is 1 + │ ├── when caller (i.e. _dropMsgSender) is not the actual token owner + │ │ └── it should revert ✅ + │ └── when caller is the actual token owner + │ ├── when the origin ERC721 contract is not burnable + │ │ └── it should revert ✅ + │ └── when the origin ERC721 contract is burnable + │ └── when mint price (i.e. pricePerToken) is zero + │ │ └── when msg.value is not zero + │ │ │ └── it should revert ✅ + │ │ └── when msg.value is zero + │ │ └── it should successfully burn the token with given tokenId for the token owner ✅ + │ │ └── it should mint new tokens to caller ✅ + │ │ └── it should emit TokensBurnedAndClaimed event ✅ + │ └── when mint price 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 successfully burn the token with given tokenId for the token owner ✅ + │ │ └── it should mint new tokens to caller ✅ + │ │ └── (transfer to sale recipient) ✅ + │ │ └── (transfer to fee recipient) ✅ + │ │ └── it should emit TokensBurnedAndClaimed event ✅ + │ └── when currency is some ERC20 token + │ └── when msg.value is not zero + │ │ └── it should revert ✅ + │ └── when msg.value is zero + │ └── it should successfully burn the token with given tokenId for the token owner ✅ + │ └── it should mint new tokens to caller ✅ + │ └── (transfer to sale recipient) ✅ + │ └── (transfer to fee recipient) ✅ + │ └── it should emit TokensBurnedAndClaimed event ✅ + └── when burn-to-claim info is set, with token type ERC1155 + ├── when `_burnTokenId` param doesn't match eligible tokenId + │ └── it should revert ✅ + └── when `_burnTokenId` param matches eligible tokenId + ├── when caller (i.e. _dropMsgSender) has balance less than quantity param + │ └── it should revert ✅ + └── when caller has balance greater than or equal to quantity param + ├── when the origin ERC1155 contract is not burnable + │ └── it should revert ✅ + └── when the origin ERC1155 contract is burnable + └── when mint price (i.e. pricePerToken) is zero + │ └── when msg.value is not zero + │ │ └── it should revert ✅ + │ └── when msg.value is zero + │ └── it should successfully burn the token with given tokenId for the token owner ✅ + │ └── it should mint new tokens to caller ✅ + │ └── it should emit TokensBurnedAndClaimed event ✅ + └── when mint price 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 successfully burn the token with given tokenId for the token owner ✅ + │ └── it should mint new tokens to caller ✅ + │ └── (transfer to sale recipient) ✅ + │ └── (transfer to fee recipient) ✅ + │ └── it should emit TokensBurnedAndClaimed event ✅ + └── when currency is some ERC20 token + └── when msg.value is not zero + │ └── it should revert ✅ + └── when msg.value is zero + └── it should successfully burn the token with given tokenId for the token owner ✅ + └── it should mint new tokens to caller ✅ + └── (transfer to sale recipient) ✅ + └── (transfer to fee recipient) ✅ + └── it should emit TokensBurnedAndClaimed event ✅ + diff --git a/src/test/burn-to-claim-drop-BTT/logic/lazy-mint/lazyMint.t.sol b/src/test/burn-to-claim-drop-BTT/logic/lazy-mint/lazyMint.t.sol new file mode 100644 index 000000000..09b0aa75b --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/logic/lazy-mint/lazyMint.t.sol @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import { BurnToClaimDropERC721 } from "contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol"; +import { BurnToClaimDrop721Logic, ERC721AUpgradeable, DelayedReveal, LazyMint, Drop, BurnToClaim, PrimarySale, PlatformFee } from "contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Logic.sol"; +import { PermissionsEnumerableImpl } from "contracts/extension/upgradeable/impl/PermissionsEnumerableImpl.sol"; +import { Royalty } from "contracts/extension/upgradeable/Royalty.sol"; +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import { IBurnToClaim } from "contracts/extension/interface/IBurnToClaim.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; +import "contracts/lib/TWStrings.sol"; + +contract BurnToClaimDropERC721Logic_LazyMint is BaseTest, IExtension { + using TWStrings for uint256; + using TWStrings for address; + + event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); + + BurnToClaimDrop721Logic public drop; + uint256 internal startId; + uint256 internal amount; + uint256[] internal batchIds; + address internal caller; + bytes internal data; + bytes internal encryptedUri; + bytes32 internal provenanceHash; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address dropImpl = address(new BurnToClaimDropERC721(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + drop = BurnToClaimDrop721Logic( + payable( + address( + new TWProxy( + dropImpl, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ) + ); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + + startId = 0; + // mint 5 batches + vm.startPrank(deployer); + for (uint256 i = 0; i < 5; i++) { + uint256 _amount = (i + 1) * 10; + uint256 batchId = startId + _amount; + batchIds.push(batchId); + + string memory baseURI = Strings.toString(batchId); + startId = drop.lazyMint(_amount, baseURI, ""); + } + vm.stopPrank(); + + encryptedUri = bytes("ipfs://encryptedURI"); + provenanceHash = keccak256("provenanceHash"); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](2); + + // Extension: Permissions + address permissions = address(new PermissionsEnumerableImpl()); + + Extension memory extension_permissions; + extension_permissions.metadata = ExtensionMetadata({ + name: "Permissions", + metadataURI: "ipfs://Permissions", + implementation: permissions + }); + + extension_permissions.functions = new ExtensionFunction[](1); + extension_permissions.functions[0] = ExtensionFunction( + Permissions.hasRole.selector, + "hasRole(bytes32,address)" + ); + + extensions[0] = extension_permissions; + + address dropLogic = address(new BurnToClaimDrop721Logic()); + + Extension memory extension_drop; + extension_drop.metadata = ExtensionMetadata({ + name: "BurnToClaimDrop721Logic", + metadataURI: "ipfs://BurnToClaimDrop721Logic", + implementation: dropLogic + }); + + extension_drop.functions = new ExtensionFunction[](6); + extension_drop.functions[0] = ExtensionFunction(BurnToClaimDrop721Logic.tokenURI.selector, "tokenURI(uint256)"); + extension_drop.functions[1] = ExtensionFunction( + BurnToClaimDrop721Logic.lazyMint.selector, + "lazyMint(uint256,string,bytes)" + ); + extension_drop.functions[2] = ExtensionFunction( + BatchMintMetadata.getBaseURICount.selector, + "getBaseURICount()" + ); + extension_drop.functions[3] = ExtensionFunction( + BurnToClaimDrop721Logic.nextTokenIdToMint.selector, + "nextTokenIdToMint()" + ); + extension_drop.functions[4] = ExtensionFunction( + DelayedReveal.encryptDecrypt.selector, + "encryptDecrypt(bytes,bytes)" + ); + extension_drop.functions[5] = ExtensionFunction( + DelayedReveal.isEncryptedBatch.selector, + "isEncryptedBatch(uint256)" + ); + + extensions[1] = extension_drop; + } + + // ================== + // ======= Test branch: when `data` empty + // ================== + + function test_lazyMint_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + drop.lazyMint(amount, "", ""); + } + + modifier whenCallerAuthorized() { + caller = deployer; + _; + } + + function test_lazyMint_zeroAmount() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectRevert("0 amt"); + drop.lazyMint(amount, "", ""); + } + + modifier whenAmountNotZero() { + amount = 50; + _; + } + + function test_lazyMint() public whenCallerAuthorized whenAmountNotZero { + // check previous state + uint256 _nextTokenIdToLazyMintOld = drop.nextTokenIdToMint(); + assertEq(_nextTokenIdToLazyMintOld, batchIds[4]); + + string memory baseURI = "ipfs://baseURI"; + + // lazy mint next batch + vm.prank(address(caller)); + uint256 _batchId = drop.lazyMint(amount, baseURI, ""); + + // check new state + assertEq(_batchId, _nextTokenIdToLazyMintOld + amount); + for (uint256 i = _nextTokenIdToLazyMintOld; i < _batchId; i++) { + assertEq(drop.tokenURI(i), string(abi.encodePacked(baseURI, i.toString()))); + } + assertEq(drop.nextTokenIdToMint(), _nextTokenIdToLazyMintOld + amount); + assertEq(drop.getBaseURICount(), batchIds.length + 1); + } + + function test_lazyMint_event() public whenCallerAuthorized whenAmountNotZero { + string memory baseURI = "ipfs://baseURI"; + uint256 _nextTokenIdToLazyMintOld = drop.nextTokenIdToMint(); + + // lazy mint next batch + vm.prank(address(caller)); + vm.expectEmit(true, false, false, true); + emit TokensLazyMinted(_nextTokenIdToLazyMintOld, _nextTokenIdToLazyMintOld + amount - 1, baseURI, ""); + drop.lazyMint(amount, baseURI, ""); + } + + // ================== + // ======= Test branch: when `data` not empty + // ================== + + function test_lazyMint_withData_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + drop.lazyMint(amount, "", data); + } + + function test_lazyMint_withData_zeroAmount() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectRevert("0 amt"); + drop.lazyMint(amount, "", data); + } + + function test_lazyMint_withData_incorrectData() public whenCallerAuthorized whenAmountNotZero { + data = bytes("random data"); // not bytes+bytes32 encoded as expected + vm.prank(address(caller)); + vm.expectRevert(); + drop.lazyMint(amount, "", data); + } + + modifier whenCorrectEncodingOfData() { + data = abi.encode(encryptedUri, provenanceHash); + _; + } + + function test_lazyMint_withData() public whenCallerAuthorized whenAmountNotZero whenCorrectEncodingOfData { + // check previous state + uint256 _nextTokenIdToLazyMintOld = drop.nextTokenIdToMint(); + assertEq(_nextTokenIdToLazyMintOld, batchIds[4]); + + string memory placeholderURI = "ipfs://placeholderURI"; + + // lazy mint next batch + vm.prank(address(caller)); + uint256 _batchId = drop.lazyMint(amount, placeholderURI, data); + + // check new state + assertTrue(drop.isEncryptedBatch(_batchId)); // encrypted batch + assertEq(_batchId, _nextTokenIdToLazyMintOld + amount); + for (uint256 i = _nextTokenIdToLazyMintOld; i < _batchId; i++) { + assertEq(drop.tokenURI(i), string(abi.encodePacked(placeholderURI, "0"))); // encrypted batch, hence token-id 0 + } + assertEq(drop.nextTokenIdToMint(), _nextTokenIdToLazyMintOld + amount); + assertEq(drop.getBaseURICount(), batchIds.length + 1); + } + + function test_lazyMint_withData_event() public whenCallerAuthorized whenAmountNotZero whenCorrectEncodingOfData { + string memory placeholderURI = "ipfs://placeholderURI"; + uint256 _nextTokenIdToLazyMintOld = drop.nextTokenIdToMint(); + + // lazy mint next batch + vm.prank(address(caller)); + vm.expectEmit(true, false, false, true); + emit TokensLazyMinted(_nextTokenIdToLazyMintOld, _nextTokenIdToLazyMintOld + amount - 1, placeholderURI, data); + drop.lazyMint(amount, placeholderURI, data); + } +} diff --git a/src/test/burn-to-claim-drop-BTT/logic/lazy-mint/lazyMint.tree b/src/test/burn-to-claim-drop-BTT/logic/lazy-mint/lazyMint.tree new file mode 100644 index 000000000..39b512286 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/logic/lazy-mint/lazyMint.tree @@ -0,0 +1,38 @@ +lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + bytes calldata _data +) +// Assume `_data` empty +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when amount to lazy mint is 0 + │ └── it should revert ✅ + └── when amount to lazy mint is not 0 + └── it should save the batch of tokens starting at `nextTokenIdToLazyMint` ✅ + └── it should store batch id equal to the sum of `nextTokenIdToLazyMint` and `_amount` ✅ + └── it should map the new batch id to `_baseURIForTokens` ✅ + └── it should increase `nextTokenIdToLazyMint` by `_amount` ✅ + └── it should return the new `batchId` ✅ + └── it should emit TokensLazyMinted event ✅ + +// Assume `_data` not empty +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when amount to lazy mint is 0 + │ └── it should revert ✅ + └── when amount to lazy mint is not 0 + └── when data can't be decoded + │ └── it should revert ✅ + └── when data can be decoded successfully + └── when decoded encryptedURI and provenanceHash are non-empty + └── it should set encrypted data for the new batch equal to _data ✅ + └── it should save the batch of tokens starting at `nextTokenIdToLazyMint` ✅ + └── it should store batch id equal to the sum of `nextTokenIdToLazyMint` and `_amount` ✅ + └── it should map the new batch id to `_baseURIForTokens` ✅ + └── it should increase `nextTokenIdToLazyMint` by `_amount` ✅ + └── it should return the new `batchId` ✅ + └── it should emit TokensLazyMinted event ✅ + diff --git a/src/test/burn-to-claim-drop-BTT/logic/other-functions/other.t.sol b/src/test/burn-to-claim-drop-BTT/logic/other-functions/other.t.sol new file mode 100644 index 000000000..95b58c798 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/logic/other-functions/other.t.sol @@ -0,0 +1,350 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import { BurnToClaimDropERC721 } from "contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol"; +import { BurnToClaimDrop721Logic, IERC2981 } from "contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Logic.sol"; +import { IDrop } from "contracts/extension/interface/IDrop.sol"; +import { IStaking721 } from "contracts/extension/interface/IStaking721.sol"; +import { PermissionsEnumerableImpl } from "contracts/extension/upgradeable/impl/PermissionsEnumerableImpl.sol"; + +import { ERC721AStorage } from "contracts/extension/upgradeable/init/ERC721AInit.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyBurnToClaimDrop721Logic is BurnToClaimDrop721Logic { + function canSetPlatformFeeInfo() external view returns (bool) { + return _canSetPlatformFeeInfo(); + } + + function canSetPrimarySaleRecipient() external view returns (bool) { + return _canSetPrimarySaleRecipient(); + } + + function canSetOwner() external view returns (bool) { + return _canSetOwner(); + } + + function canSetRoyaltyInfo() external view returns (bool) { + return _canSetRoyaltyInfo(); + } + + function canSetContractURI() external view returns (bool) { + return _canSetContractURI(); + } + + function canSetClaimConditions() external view returns (bool) { + return _canSetClaimConditions(); + } + + function canLazyMint() external view returns (bool) { + return _canLazyMint(); + } + + function canSetBurnToClaim() external view returns (bool) { + return _canSetBurnToClaim(); + } + + function beforeTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) external { + _beforeTokenTransfers(from, to, startTokenId, quantity); + } + + function transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) external returns (uint256 startTokenId) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + startTokenId = data._currentIndex; + _safeMint(_to, _quantityBeingClaimed); + } + + function beforeClaim(uint256 _quantity, AllowlistProof calldata proof) external { + _beforeClaim(address(0), _quantity, address(0), 0, proof, ""); + } +} + +contract BurnToClaimDrop721Logic_OtherFunctions is BaseTest, IExtension { + MyBurnToClaimDrop721Logic public drop; + address internal caller; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address dropImpl = address(new BurnToClaimDropERC721(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + drop = MyBurnToClaimDrop721Logic( + payable( + address( + new TWProxy( + dropImpl, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ) + ); + + caller = getActor(5); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](2); + + // Extension: Permissions + address permissions = address(new PermissionsEnumerableImpl()); + + Extension memory extension_permissions; + extension_permissions.metadata = ExtensionMetadata({ + name: "Permissions", + metadataURI: "ipfs://Permissions", + implementation: permissions + }); + + extension_permissions.functions = new ExtensionFunction[](3); + extension_permissions.functions[0] = ExtensionFunction( + Permissions.hasRole.selector, + "hasRole(bytes32,address)" + ); + extension_permissions.functions[1] = ExtensionFunction( + Permissions.grantRole.selector, + "grantRole(bytes32,address)" + ); + extension_permissions.functions[2] = ExtensionFunction( + Permissions.revokeRole.selector, + "revokeRole(bytes32,address)" + ); + + extensions[0] = extension_permissions; + + address dropLogic = address(new MyBurnToClaimDrop721Logic()); + + Extension memory extension_drop; + extension_drop.metadata = ExtensionMetadata({ + name: "MyBurnToClaimDrop721Logic", + metadataURI: "ipfs://MyBurnToClaimDrop721Logic", + implementation: dropLogic + }); + + extension_drop.functions = new ExtensionFunction[](15); + extension_drop.functions[0] = ExtensionFunction( + MyBurnToClaimDrop721Logic.canSetPlatformFeeInfo.selector, + "canSetPlatformFeeInfo()" + ); + extension_drop.functions[1] = ExtensionFunction( + MyBurnToClaimDrop721Logic.canSetPrimarySaleRecipient.selector, + "canSetPrimarySaleRecipient()" + ); + extension_drop.functions[2] = ExtensionFunction( + MyBurnToClaimDrop721Logic.canSetOwner.selector, + "canSetOwner()" + ); + extension_drop.functions[3] = ExtensionFunction( + MyBurnToClaimDrop721Logic.canSetRoyaltyInfo.selector, + "canSetRoyaltyInfo()" + ); + extension_drop.functions[4] = ExtensionFunction( + MyBurnToClaimDrop721Logic.canSetClaimConditions.selector, + "canSetClaimConditions()" + ); + extension_drop.functions[5] = ExtensionFunction( + MyBurnToClaimDrop721Logic.canSetContractURI.selector, + "canSetContractURI()" + ); + extension_drop.functions[6] = ExtensionFunction( + MyBurnToClaimDrop721Logic.canLazyMint.selector, + "canLazyMint()" + ); + extension_drop.functions[7] = ExtensionFunction( + MyBurnToClaimDrop721Logic.canSetBurnToClaim.selector, + "canSetBurnToClaim()" + ); + extension_drop.functions[8] = ExtensionFunction( + MyBurnToClaimDrop721Logic.beforeTokenTransfers.selector, + "beforeTokenTransfers(address,address,uint256,uint256)" + ); + extension_drop.functions[9] = ExtensionFunction(BurnToClaimDrop721Logic.totalMinted.selector, "totalMinted()"); + extension_drop.functions[10] = ExtensionFunction( + MyBurnToClaimDrop721Logic.transferTokensOnClaim.selector, + "transferTokensOnClaim(address,uint256)" + ); + extension_drop.functions[11] = ExtensionFunction( + BurnToClaimDrop721Logic.supportsInterface.selector, + "supportsInterface(bytes4)" + ); + extension_drop.functions[12] = ExtensionFunction( + MyBurnToClaimDrop721Logic.beforeClaim.selector, + "beforeClaim(uint256,(bytes32[],uint256,uint256,address))" + ); + extension_drop.functions[13] = ExtensionFunction( + BurnToClaimDrop721Logic.lazyMint.selector, + "lazyMint(uint256,string,bytes)" + ); + extension_drop.functions[14] = ExtensionFunction( + BurnToClaimDrop721Logic.setMaxTotalMinted.selector, + "setMaxTotalMinted(uint256)" + ); + + extensions[1] = extension_drop; + } + + modifier whenCallerAuthorized() { + caller = deployer; + _; + } + + function test_canSetPlatformFeeInfo_notAuthorized() public { + vm.prank(caller); + drop.canSetPlatformFeeInfo(); + } + + function test_canSetPlatformFeeInfo() public whenCallerAuthorized { + vm.prank(caller); + assertTrue(drop.canSetPlatformFeeInfo()); + } + + function test_canSetPrimarySaleRecipient_notAuthorized() public { + vm.prank(caller); + drop.canSetPrimarySaleRecipient(); + } + + function test_canSetPrimarySaleRecipient() public whenCallerAuthorized { + vm.prank(caller); + assertTrue(drop.canSetPrimarySaleRecipient()); + } + + function test_canSetOwner_notAuthorized() public { + vm.prank(caller); + drop.canSetOwner(); + } + + function test_canSetOwner() public whenCallerAuthorized { + vm.prank(caller); + assertTrue(drop.canSetOwner()); + } + + function test_canSetRoyaltyInfo_notAuthorized() public { + vm.prank(caller); + drop.canSetRoyaltyInfo(); + } + + function test_canSetRoyaltyInfo() public whenCallerAuthorized { + vm.prank(caller); + assertTrue(drop.canSetRoyaltyInfo()); + } + + function test_canSetContractURI_notAuthorized() public { + vm.prank(caller); + drop.canSetContractURI(); + } + + function test_canSetContractURI() public whenCallerAuthorized { + vm.prank(caller); + assertTrue(drop.canSetContractURI()); + } + + function test_canSetClaimConditions_notAuthorized() public { + vm.prank(caller); + drop.canSetClaimConditions(); + } + + function test_canSetClaimConditions() public whenCallerAuthorized { + vm.prank(caller); + assertTrue(drop.canSetClaimConditions()); + } + + function test_canLazyMint_notAuthorized() public { + vm.prank(caller); + drop.canLazyMint(); + } + + function test_canLazyMint() public whenCallerAuthorized { + vm.prank(caller); + assertTrue(drop.canLazyMint()); + } + + function test_canSetBurnToClaim_notAuthorized() public { + vm.prank(caller); + drop.canSetBurnToClaim(); + } + + function test_canSetBurnToClaim() public whenCallerAuthorized { + vm.prank(caller); + assertTrue(drop.canSetBurnToClaim()); + } + + function test_beforeTokenTransfers_restricted_notTransferRole() public { + vm.prank(deployer); + Permissions(address(drop)).revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + vm.expectRevert("!Transfer-Role"); + drop.beforeTokenTransfers(caller, address(0x123), 0, 1); + } + + modifier whenTransferRole() { + vm.prank(deployer); + Permissions(address(drop)).grantRole(keccak256("TRANSFER_ROLE"), caller); + _; + } + + function test_beforeTokenTransfers_restricted() public whenTransferRole { + drop.beforeTokenTransfers(caller, address(0x123), 0, 1); + } + + function test_totalMinted() public { + uint256 totalMinted = drop.totalMinted(); + assertEq(totalMinted, 0); + + // mint tokens + drop.transferTokensOnClaim(caller, 10); + totalMinted = drop.totalMinted(); + assertEq(totalMinted, 10); + } + + function test_supportsInterface() public { + assertTrue(drop.supportsInterface(type(IERC2981).interfaceId)); + assertFalse(drop.supportsInterface(type(IStaking721).interfaceId)); + } + + function test_beforeClaim() public { + bytes32[] memory emptyBytes32Array = new bytes32[](0); + IDrop.AllowlistProof memory proof = IDrop.AllowlistProof(emptyBytes32Array, 0, 0, address(0)); + drop.beforeClaim(0, proof); + + vm.expectRevert("!Tokens"); + drop.beforeClaim(1, proof); + + vm.prank(deployer); + drop.lazyMint(100, "ipfs://", ""); + + vm.prank(deployer); + drop.setMaxTotalMinted(1); + + vm.expectRevert("exceed max total mint cap."); + drop.beforeClaim(10, proof); + + vm.prank(deployer); + drop.setMaxTotalMinted(0); + + drop.beforeClaim(10, proof); // no revert if max total mint cap is set to 0 + } +} diff --git a/src/test/burn-to-claim-drop-BTT/logic/other-functions/other.tree b/src/test/burn-to-claim-drop-BTT/logic/other-functions/other.tree new file mode 100644 index 000000000..8eb392317 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/logic/other-functions/other.tree @@ -0,0 +1,84 @@ +_canSetPlatformFeeInfo() +├── when the caller doesn't have DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when the caller has DEFAULT_ADMIN_ROLE + └── it should return true ✅ + +_canSetPrimarySaleRecipient() +├── when the caller doesn't have DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when the caller has DEFAULT_ADMIN_ROLE + └── it should return true ✅ + +_canSetOwner() +├── when the caller doesn't have DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when the caller has DEFAULT_ADMIN_ROLE + └── it should return true ✅ + +_canSetRoyaltyInfo() +├── when the caller doesn't have DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when the caller has DEFAULT_ADMIN_ROLE + └── it should return true ✅ + +_canSetContractURI() +├── when the caller doesn't have DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when the caller has DEFAULT_ADMIN_ROLE + └── it should return true ✅ + +_canSetClaimConditions() +├── when the caller doesn't have DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when the caller has DEFAULT_ADMIN_ROLE + └── it should return true ✅ + +_canLazyMint() +├── when the caller doesn't have MINTER_ROLE +│ └── it should revert ✅ +└── when the caller has MINTER_ROLE + └── it should return true ✅ + +_canSetBurnToClaim() +├── when the caller doesn't have DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when the caller has DEFAULT_ADMIN_ROLE + └── it should return true ✅ + +burn(uint256 tokenId) +├── when the caller isn't the owner of `tokenId` +│ └── it should revert ✅ +└── when the caller owns `tokenId` + └── it should burn the token ✅ + +_beforeTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity +) +├── 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 ✅ + +totalMinted() +├── should return the quantity of tokens minted (i.e. claimed) so far ✅ + +supportsInterface(bytes4 interfaceId) +├── it should return true for supported interface ✅ +├── it should return false for not supported interface ✅ + +_beforeClaim( + address, + uint256 _quantity, + address, + uint256, + AllowlistProof calldata, + bytes memory +) +├── when `_quantity` exceeds lazy minted quantity +│ └── it should revert ✅ +├── when `_quantity` exceeds max total mint cap (if not zero) +│ └── it should revert ✅ + diff --git a/src/test/burn-to-claim-drop-BTT/logic/reveal/reveal.t.sol b/src/test/burn-to-claim-drop-BTT/logic/reveal/reveal.t.sol new file mode 100644 index 000000000..b8936f541 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/logic/reveal/reveal.t.sol @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import { BurnToClaimDropERC721 } from "contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol"; +import { BurnToClaimDrop721Logic, ERC721AUpgradeable, DelayedReveal, LazyMint, Drop, BurnToClaim, PrimarySale, PlatformFee } from "contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Logic.sol"; +import { PermissionsEnumerableImpl } from "contracts/extension/upgradeable/impl/PermissionsEnumerableImpl.sol"; +import { Royalty } from "contracts/extension/upgradeable/Royalty.sol"; +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import { IBurnToClaim } from "contracts/extension/interface/IBurnToClaim.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "erc721a-upgradeable/contracts/IERC721AUpgradeable.sol"; +import "contracts/lib/TWStrings.sol"; + +contract BurnToClaimDropERC721Logic_Reveal is BaseTest, IExtension { + using TWStrings for uint256; + using TWStrings for address; + + event TokenURIRevealed(uint256 indexed index, string revealedURI); + + BurnToClaimDrop721Logic public drop; + uint256 internal startId; + uint256 internal amount; + uint256[] internal batchIds; + address internal caller; + bytes internal data; + string internal placeholderURI; + bytes internal originalURI; + uint256 internal _index; + bytes internal _key; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); + address dropImpl = address(new BurnToClaimDropERC721(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + drop = BurnToClaimDrop721Logic( + payable( + address( + new TWProxy( + dropImpl, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ) + ) + ); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + + startId = 0; + originalURI = bytes("ipfs://originalURI"); + placeholderURI = "ipfs://placeholderURI"; + _key = "key123"; + // mint 3 batches + vm.startPrank(deployer); + for (uint256 i = 0; i < 3; i++) { + uint256 _amount = (i + 1) * 10; + uint256 batchId = startId + _amount; + batchIds.push(batchId); + + // set encrypted uri for one of the batches + if (i == 1) { + bytes memory _encryptedURI = drop.encryptDecrypt(originalURI, _key); + bytes32 _provenanceHash = keccak256(abi.encodePacked(originalURI, _key, block.chainid)); + + startId = drop.lazyMint(_amount, placeholderURI, abi.encode(_encryptedURI, _provenanceHash)); + } else { + startId = drop.lazyMint(_amount, string(originalURI), ""); + } + } + vm.stopPrank(); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](2); + + // Extension: Permissions + address permissions = address(new PermissionsEnumerableImpl()); + + Extension memory extension_permissions; + extension_permissions.metadata = ExtensionMetadata({ + name: "Permissions", + metadataURI: "ipfs://Permissions", + implementation: permissions + }); + + extension_permissions.functions = new ExtensionFunction[](1); + extension_permissions.functions[0] = ExtensionFunction( + Permissions.hasRole.selector, + "hasRole(bytes32,address)" + ); + + extensions[0] = extension_permissions; + + address dropLogic = address(new BurnToClaimDrop721Logic()); + + Extension memory extension_drop; + extension_drop.metadata = ExtensionMetadata({ + name: "BurnToClaimDrop721Logic", + metadataURI: "ipfs://BurnToClaimDrop721Logic", + implementation: dropLogic + }); + + extension_drop.functions = new ExtensionFunction[](6); + extension_drop.functions[0] = ExtensionFunction(BurnToClaimDrop721Logic.tokenURI.selector, "tokenURI(uint256)"); + extension_drop.functions[1] = ExtensionFunction( + BurnToClaimDrop721Logic.lazyMint.selector, + "lazyMint(uint256,string,bytes)" + ); + extension_drop.functions[2] = ExtensionFunction( + BurnToClaimDrop721Logic.reveal.selector, + "reveal(uint256,bytes)" + ); + extension_drop.functions[3] = ExtensionFunction( + DelayedReveal.encryptDecrypt.selector, + "encryptDecrypt(bytes,bytes)" + ); + extension_drop.functions[4] = ExtensionFunction( + DelayedReveal.isEncryptedBatch.selector, + "isEncryptedBatch(uint256)" + ); + extension_drop.functions[5] = ExtensionFunction( + DelayedReveal.getRevealURI.selector, + "getRevealURI(uint256,bytes)" + ); + + extensions[1] = extension_drop; + } + + function test_reveal_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + drop.lazyMint(amount, "", ""); + } + + modifier whenCallerAuthorized() { + caller = deployer; + _; + } + + function test_reveal_invalidIndex() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectRevert("Invalid index"); + drop.reveal(4, "key"); + } + + modifier whenValidIndex() { + _; + } + + function test_reveal_noEncryptedURI() public whenCallerAuthorized whenValidIndex { + _index = 2; + vm.prank(address(caller)); + vm.expectRevert("Nothing to reveal"); + drop.reveal(_index, "key"); + } + + modifier whenEncryptedURI() { + _index = 1; + _; + } + + function test_reveal_incorrectKey() public whenCallerAuthorized whenValidIndex whenEncryptedURI { + vm.prank(address(caller)); + vm.expectRevert("Incorrect key"); + drop.reveal(_index, "incorrect key"); + } + + modifier whenCorrectKey() { + _; + } + + function test_reveal() public whenCallerAuthorized whenValidIndex whenEncryptedURI { + //state before + for (uint256 i = 0; i < 3; i++) { + uint256 _startId = i > 0 ? batchIds[i - 1] : 0; + + for (uint256 j = _startId; j < batchIds[i]; j += 1) { + string memory uri = drop.tokenURI(j); + if (i == 1) { + assertEq(uri, string(abi.encodePacked(placeholderURI, "0"))); // <-- placeholder URI for encrypted batch + } else { + assertEq(uri, string(abi.encodePacked(string(originalURI), j.toString()))); + } + } + } + + // reveal + vm.prank(address(caller)); + string memory revealedURI = drop.reveal(_index, _key); + + // check state after + vm.expectRevert("Nothing to reveal"); + drop.getRevealURI(_index, _key); + + assertEq(revealedURI, string(originalURI)); + + for (uint256 i = 0; i < 3; i++) { + uint256 _startId = i > 0 ? batchIds[i - 1] : 0; + + for (uint256 j = _startId; j < batchIds[i]; j += 1) { + string memory uri = drop.tokenURI(j); + assertEq(uri, string(abi.encodePacked(string(originalURI), j.toString()))); + } + } + } + + function test_reveal_event() public whenCallerAuthorized whenValidIndex whenEncryptedURI { + vm.prank(address(caller)); + vm.expectEmit(true, false, false, true); + emit TokenURIRevealed(1, string(originalURI)); + drop.reveal(_index, _key); + } +} diff --git a/src/test/burn-to-claim-drop-BTT/logic/reveal/reveal.tree b/src/test/burn-to-claim-drop-BTT/logic/reveal/reveal.tree new file mode 100644 index 000000000..febcdb258 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/logic/reveal/reveal.tree @@ -0,0 +1,16 @@ +reveal(uint256 index, bytes calldata key) +├── when caller doesn't have minter_role +│ └── it should revert ✅ +└── when caller has minter role + ├── when index is more than number of batches + │ └── it should revert ✅ + └── when index is within total number of batches + ├── when there is no encrypted uri associated with the batch index + │ └── it should revert ✅ + └── when there is an encrypted uri present + ├── when the provenance hash generated is incorrect for the given key + │ └── it should revert ✅ + └── when provenance hash is correct + └── it should set the encrypted data for this batch to "" ✅ + └── it should set base URI for this batch to correct revealed URI ✅ + └── it should emit TokenURIRevealed event ✅ \ No newline at end of file diff --git a/src/test/burn-to-claim-drop-BTT/router/initialize/initialize.t.sol b/src/test/burn-to-claim-drop-BTT/router/initialize/initialize.t.sol new file mode 100644 index 000000000..cb1da0318 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/router/initialize/initialize.t.sol @@ -0,0 +1,455 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import { BurnToClaimDropERC721 } from "contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol"; +import { BurnToClaimDrop721Logic } from "contracts/prebuilts/unaudited/burn-to-claim-drop/extension/BurnToClaimDrop721Logic.sol"; +import { PermissionsEnumerableImpl } from "contracts/extension/upgradeable/impl/PermissionsEnumerableImpl.sol"; + +import { ERC721AStorage } from "contracts/extension/upgradeable/init/ERC721AInit.sol"; +import { ERC2771ContextStorage } from "contracts/extension/upgradeable/init/ERC2771ContextInit.sol"; +import { ContractMetadataStorage } from "contracts/extension/upgradeable/init/ContractMetadataInit.sol"; +import { OwnableStorage } from "contracts/extension/upgradeable/init/OwnableInit.sol"; +import { PlatformFeeStorage } from "contracts/extension/upgradeable/init/PlatformFeeInit.sol"; +import { RoyaltyStorage } from "contracts/extension/upgradeable/init/RoyaltyInit.sol"; +import { PrimarySaleStorage } from "contracts/extension/upgradeable/init/PrimarySaleInit.sol"; +import { PermissionsStorage } from "contracts/extension/upgradeable/init/PermissionsInit.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract BurnToClaimDropERC721Router is BurnToClaimDropERC721 { + constructor(Extension[] memory _extensions) BurnToClaimDropERC721(_extensions) {} + + function hasRole(bytes32 role, address addr) public view returns (bool) { + return _hasRole(role, addr); + } + + function roleAdmin(bytes32 role) public view returns (bytes32) { + PermissionsStorage.Data storage data = PermissionsStorage.data(); + return data._getRoleAdmin[role]; + } + + function name() public view returns (string memory) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + return data._name; + } + + function symbol() public view returns (string memory) { + ERC721AStorage.Data storage data = ERC721AStorage.erc721AStorage(); + return data._symbol; + } + + function trustedForwarders(address[] memory _trustedForwarders) public view returns (bool) { + ERC2771ContextStorage.Data storage data = ERC2771ContextStorage.data(); + + for (uint256 i = 0; i < _trustedForwarders.length; i++) { + if (!data.trustedForwarder[_trustedForwarders[i]]) { + return false; + } + } + return true; + } + + function contractURI() public view returns (string memory) { + ContractMetadataStorage.Data storage data = ContractMetadataStorage.data(); + return data.contractURI; + } + + function owner() public view returns (address) { + OwnableStorage.Data storage data = OwnableStorage.data(); + return data._owner; + } + + function platformFeeRecipient() public view returns (address) { + PlatformFeeStorage.Data storage data = PlatformFeeStorage.data(); + return data.platformFeeRecipient; + } + + function platformFeeBps() public view returns (uint16) { + PlatformFeeStorage.Data storage data = PlatformFeeStorage.data(); + return data.platformFeeBps; + } + + function royaltyRecipient() public view returns (address) { + RoyaltyStorage.Data storage data = RoyaltyStorage.data(); + return data.royaltyRecipient; + } + + function royaltyBps() public view returns (uint16) { + RoyaltyStorage.Data storage data = RoyaltyStorage.data(); + return data.royaltyBps; + } + + function primarySaleRecipient() public view returns (address) { + PrimarySaleStorage.Data storage data = PrimarySaleStorage.data(); + return data.recipient; + } +} + +contract BurnToClaimDropERC721_Initialize is BaseTest, IExtension { + address public implementation; + address public proxy; + + 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 RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps); + event PrimarySaleRecipientUpdated(address indexed recipient); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + Extension[] memory extensions = _setupExtensions(); // setup just a couple of extension/functions for testing here + implementation = address(new BurnToClaimDropERC721Router(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + } + + function _setupExtensions() internal returns (Extension[] memory extensions) { + extensions = new Extension[](2); + + // Extension: Permissions + address permissions = address(new PermissionsEnumerableImpl()); + + Extension memory extension_permissions; + extension_permissions.metadata = ExtensionMetadata({ + name: "Permissions", + metadataURI: "ipfs://Permissions", + implementation: permissions + }); + + extension_permissions.functions = new ExtensionFunction[](1); + extension_permissions.functions[0] = ExtensionFunction( + Permissions.hasRole.selector, + "hasRole(bytes32,address)" + ); + + extensions[0] = extension_permissions; + + address dropLogic = address(new BurnToClaimDrop721Logic()); + + Extension memory extension_drop; + extension_drop.metadata = ExtensionMetadata({ + name: "BurnToClaimDrop721Logic", + metadataURI: "ipfs://BurnToClaimDrop721Logic", + implementation: dropLogic + }); + + extension_drop.functions = new ExtensionFunction[](1); + extension_drop.functions[0] = ExtensionFunction(BurnToClaimDrop721Logic.tokenURI.selector, "tokenURI(uint256)"); + extensions[1] = extension_drop; + } + + function test_initialize_initializingImplementation() public { + vm.expectRevert("Initializable: contract is already initialized"); + BurnToClaimDropERC721Router(payable(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"); + BurnToClaimDropERC721Router(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + modifier whenProxyNotInitialized() { + proxy = address(new TWProxy(implementation, "")); + _; + } + + function test_initialize() public whenNotImplementation whenProxyNotInitialized { + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + + // check state + BurnToClaimDropERC721Router router = BurnToClaimDropERC721Router(payable(proxy)); + assertEq(router.name(), NAME); + assertEq(router.symbol(), SYMBOL); + assertTrue(router.trustedForwarders(forwarders())); + assertEq(router.platformFeeRecipient(), platformFeeRecipient); + assertEq(router.platformFeeBps(), platformFeeBps); + assertEq(router.royaltyRecipient(), royaltyRecipient); + assertEq(router.royaltyBps(), royaltyBps); + assertEq(router.primarySaleRecipient(), saleRecipient); + assertTrue(router.hasRole(bytes32(0x00), deployer)); + assertTrue(router.hasRole(keccak256("TRANSFER_ROLE"), deployer)); + assertTrue(router.hasRole(keccak256("TRANSFER_ROLE"), address(0))); + assertTrue(router.hasRole(keccak256("MINTER_ROLE"), deployer)); + assertTrue(router.hasRole(keccak256("EXTENSION_ROLE"), deployer)); + assertEq(router.roleAdmin(keccak256("EXTENSION_ROLE")), keccak256("EXTENSION_ROLE")); + + // check default extensions + Extension[] memory _extensions = router.getAllExtensions(); + assertEq(_extensions.length, 2); + } + + function test_initialize_event_ContractURIUpdated() public whenNotImplementation whenProxyNotInitialized { + vm.expectEmit(false, false, false, true); + emit ContractURIUpdated("", CONTRACT_URI); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_OwnerUpdated() public whenNotImplementation whenProxyNotInitialized { + vm.expectEmit(true, true, false, false); + emit OwnerUpdated(address(0), deployer); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_DefaultAdmin() public whenNotImplementation whenProxyNotInitialized { + bytes32 _defaultAdminRole = bytes32(0x00); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_defaultAdminRole, deployer, deployer); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_MinterRole() public whenNotImplementation whenProxyNotInitialized { + bytes32 _minterRole = keccak256("MINTER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_minterRole, deployer, deployer); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_TransferRole() public whenNotImplementation whenProxyNotInitialized { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_transferRole, deployer, deployer); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_TransferRole_AddressZero() + public + whenNotImplementation + whenProxyNotInitialized + { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_transferRole, address(0), deployer); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleGranted_ExtensionRole() public whenNotImplementation whenProxyNotInitialized { + bytes32 _extensionRole = keccak256("EXTENSION_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_extensionRole, deployer, deployer); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_RoleAdminChanged_ExtensionRole() + public + whenNotImplementation + whenProxyNotInitialized + { + bytes32 _extensionRole = keccak256("EXTENSION_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleAdminChanged(_extensionRole, bytes32(0x00), _extensionRole); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_PlatformFeeInfoUpdated() public whenNotImplementation whenProxyNotInitialized { + vm.prank(deployer); + vm.expectEmit(true, false, false, true); + emit PlatformFeeInfoUpdated(platformFeeRecipient, platformFeeBps); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_DefaultRoyalty() public whenNotImplementation whenProxyNotInitialized { + vm.prank(deployer); + vm.expectEmit(true, false, false, true); + emit DefaultRoyalty(royaltyRecipient, royaltyBps); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } + + function test_initialize_event_PrimarySaleRecipientUpdated() public whenNotImplementation whenProxyNotInitialized { + vm.prank(deployer); + vm.expectEmit(true, false, false, false); + emit PrimarySaleRecipientUpdated(saleRecipient); + BurnToClaimDropERC721(payable(proxy)).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ); + } +} diff --git a/src/test/burn-to-claim-drop-BTT/router/initialize/initialize.tree b/src/test/burn-to-claim-drop-BTT/router/initialize/initialize.tree new file mode 100644 index 000000000..295d65120 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/router/initialize/initialize.tree @@ -0,0 +1,44 @@ +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 it is 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 + └── it should initialize base-router with default extensions if any ✅ + └── 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 emit ContractURIUpdated event ✅ + └── it should set _owner to `_defaultAdmin` param value ✅ + └── it should emit OwnerUpdated event ✅ + └── 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 EXTENSION_ROLE to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should set EXTENSION_ROLE as role admin for EXTENSION_ROLE ✅ + └── it should emit RoleAdminChanged event ✅ + └── it should set platformFeeRecipient and platformFeeBps as `_platformFeeRecipient` and `_platformFeeBps` respectively ✅ + └── it should emit PlatformFeeInfoUpdated event ✅ + └── it should set royaltyRecipient and royaltyBps as `_royaltyRecipient` and `_royaltyBps` respectively ✅ + └── it should emit DefaultRoyalty event ✅ + └── it should set primary sale recipient as `_saleRecipient` param value ✅ + └── it should emit PrimarySaleRecipientUpdated event ✅ + diff --git a/src/test/burn-to-claim-drop-BTT/router/other-functions/other.t.sol b/src/test/burn-to-claim-drop-BTT/router/other-functions/other.t.sol new file mode 100644 index 000000000..d9cdb0656 --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/router/other-functions/other.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../../utils/BaseTest.sol"; +import { BurnToClaimDropERC721 } from "contracts/prebuilts/unaudited/burn-to-claim-drop/BurnToClaimDropERC721.sol"; + +import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract BurnToClaimDropERC721Router is BurnToClaimDropERC721 { + constructor(Extension[] memory _extensions) BurnToClaimDropERC721(_extensions) {} + + function isAuthorizedCallToUpgrade() public view returns (bool) { + return _isAuthorizedCallToUpgrade(); + } +} + +contract BurnToClaimDropERC721_OtherFunctions is BaseTest, IExtension { + address public implementation; + address public proxy; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + Extension[] memory extensions; + implementation = address(new BurnToClaimDropERC721Router(extensions)); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + BurnToClaimDropERC721.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + royaltyRecipient, + royaltyBps, + platformFeeBps, + platformFeeRecipient + ) + ) + ) + ); + } + + function test_contractType() public { + BurnToClaimDropERC721Router router = BurnToClaimDropERC721Router(payable(proxy)); + assertEq(router.contractType(), bytes32("BurnToClaimDropERC721")); + } + + function test_contractVersion() public { + BurnToClaimDropERC721Router router = BurnToClaimDropERC721Router(payable(proxy)); + assertEq(router.contractVersion(), uint8(5)); + } + + function test_isAuthorizedCallToUpgrade_notExtensionRole() public { + BurnToClaimDropERC721Router router = BurnToClaimDropERC721Router(payable(proxy)); + assertFalse(router.isAuthorizedCallToUpgrade()); + } + + modifier whenExtensionRole() { + _; + } + + function test_isAuthorizedCallToUpgrade() public whenExtensionRole { + BurnToClaimDropERC721Router router = BurnToClaimDropERC721Router(payable(proxy)); + + vm.prank(deployer); + assertTrue(router.isAuthorizedCallToUpgrade()); + } +} diff --git a/src/test/burn-to-claim-drop-BTT/router/other-functions/other.tree b/src/test/burn-to-claim-drop-BTT/router/other-functions/other.tree new file mode 100644 index 000000000..98e8df4fe --- /dev/null +++ b/src/test/burn-to-claim-drop-BTT/router/other-functions/other.tree @@ -0,0 +1,12 @@ +contractType() +├── it should return bytes32("BurnToClaimDropERC721") ✅ + +contractVersion() +├── it should return uint8(5) ✅ + +_isAuthorizedCallToUpgrade() +├── when the caller doesn't have EXTENSION_ROLE +│ └── it should revert ✅ +└── when the caller has EXTENSION_ROLE + └── it should return true ✅ + diff --git a/src/test/drop/drop-erc1155/initialize/initialize.t.sol b/src/test/drop/drop-erc1155/initialize/initialize.t.sol index bf4dba6bf..0dff9ca09 100644 --- a/src/test/drop/drop-erc1155/initialize/initialize.t.sol +++ b/src/test/drop/drop-erc1155/initialize/initialize.t.sol @@ -164,7 +164,7 @@ contract DropERC1155Test_initializer is BaseTest { function test_event_RoleGrantedDefaultAdminRole() public { bytes32 role = bytes32(0x00); vm.expectEmit(true, true, true, false); - emit RoleGranted(role, deployer, 0xf29D12e4c9d593D2887EEDDe076bBe39EDf3cD0F); + emit RoleGranted(role, deployer, factory); deployContractProxy( "DropERC1155", abi.encodeCall( @@ -188,7 +188,7 @@ contract DropERC1155Test_initializer is BaseTest { function test_event_RoleGrantedMinterRole() public { bytes32 role = keccak256("MINTER_ROLE"); vm.expectEmit(true, true, true, false); - emit RoleGranted(role, deployer, 0xf29D12e4c9d593D2887EEDDe076bBe39EDf3cD0F); + emit RoleGranted(role, deployer, factory); deployContractProxy( "DropERC1155", abi.encodeCall( @@ -212,7 +212,7 @@ contract DropERC1155Test_initializer is BaseTest { function test_event_RoleGrantedTransferRole() public { bytes32 role = keccak256("TRANSFER_ROLE"); vm.expectEmit(true, true, true, false); - emit RoleGranted(role, deployer, 0xf29D12e4c9d593D2887EEDDe076bBe39EDf3cD0F); + emit RoleGranted(role, deployer, factory); deployContractProxy( "DropERC1155", abi.encodeCall( @@ -236,7 +236,7 @@ contract DropERC1155Test_initializer is BaseTest { function test_event_RoleGrantedTransferRoleZeroAddress() public { bytes32 role = keccak256("TRANSFER_ROLE"); vm.expectEmit(true, true, true, false); - emit RoleGranted(role, address(0), 0xf29D12e4c9d593D2887EEDDe076bBe39EDf3cD0F); + emit RoleGranted(role, address(0), factory); deployContractProxy( "DropERC1155", abi.encodeCall( @@ -260,7 +260,7 @@ contract DropERC1155Test_initializer is BaseTest { function test_event_RoleGrantedMetadataRole() public { bytes32 role = keccak256("METADATA_ROLE"); vm.expectEmit(true, true, true, false); - emit RoleGranted(role, deployer, 0xf29D12e4c9d593D2887EEDDe076bBe39EDf3cD0F); + emit RoleGranted(role, deployer, factory); deployContractProxy( "DropERC1155", abi.encodeCall( diff --git a/src/test/drop/drop-erc20/initialize/initialize.t.sol b/src/test/drop/drop-erc20/initialize/initialize.t.sol index 74714744e..c6a6ddbd6 100644 --- a/src/test/drop/drop-erc20/initialize/initialize.t.sol +++ b/src/test/drop/drop-erc20/initialize/initialize.t.sol @@ -101,7 +101,7 @@ contract DropERC20Test_initializer is BaseTest { function test_event_RoleGrantedDefaultAdminRole() public { bytes32 role = bytes32(0x00); vm.expectEmit(true, true, true, false); - emit RoleGranted(role, deployer, 0xf29D12e4c9d593D2887EEDDe076bBe39EDf3cD0F); + emit RoleGranted(role, deployer, factory); deployContractProxy( "DropERC20", abi.encodeCall( @@ -123,7 +123,7 @@ contract DropERC20Test_initializer is BaseTest { function test_event_RoleGrantedTransferRole() public { bytes32 role = keccak256("TRANSFER_ROLE"); vm.expectEmit(true, true, true, false); - emit RoleGranted(role, deployer, 0xf29D12e4c9d593D2887EEDDe076bBe39EDf3cD0F); + emit RoleGranted(role, deployer, factory); deployContractProxy( "DropERC20", abi.encodeCall( @@ -145,7 +145,7 @@ contract DropERC20Test_initializer is BaseTest { function test_event_RoleGrantedTransferRoleZeroAddress() public { bytes32 role = keccak256("TRANSFER_ROLE"); vm.expectEmit(true, true, true, false); - emit RoleGranted(role, address(0), 0xf29D12e4c9d593D2887EEDDe076bBe39EDf3cD0F); + emit RoleGranted(role, address(0), factory); deployContractProxy( "DropERC20", abi.encodeCall( diff --git a/src/test/drop/drop-erc721/initalizer/initializer.t.sol b/src/test/drop/drop-erc721/initalizer/initializer.t.sol index 4c035da97..b43fb881d 100644 --- a/src/test/drop/drop-erc721/initalizer/initializer.t.sol +++ b/src/test/drop/drop-erc721/initalizer/initializer.t.sol @@ -164,7 +164,7 @@ contract DropERC721Test_initializer is BaseTest { function test_event_RoleGrantedDefaultAdminRole() public { bytes32 role = bytes32(0x00); vm.expectEmit(true, true, true, false); - emit RoleGranted(role, deployer, 0xf29D12e4c9d593D2887EEDDe076bBe39EDf3cD0F); + emit RoleGranted(role, deployer, factory); deployContractProxy( "DropERC721", abi.encodeCall( @@ -188,7 +188,7 @@ contract DropERC721Test_initializer is BaseTest { function test_event_RoleGrantedMinterRole() public { bytes32 role = keccak256("MINTER_ROLE"); vm.expectEmit(true, true, true, false); - emit RoleGranted(role, deployer, 0xf29D12e4c9d593D2887EEDDe076bBe39EDf3cD0F); + emit RoleGranted(role, deployer, factory); deployContractProxy( "DropERC721", abi.encodeCall( @@ -212,7 +212,7 @@ contract DropERC721Test_initializer is BaseTest { function test_event_RoleGrantedTransferRole() public { bytes32 role = keccak256("TRANSFER_ROLE"); vm.expectEmit(true, true, true, false); - emit RoleGranted(role, deployer, 0xf29D12e4c9d593D2887EEDDe076bBe39EDf3cD0F); + emit RoleGranted(role, deployer, factory); deployContractProxy( "DropERC721", abi.encodeCall( @@ -236,7 +236,7 @@ contract DropERC721Test_initializer is BaseTest { function test_event_RoleGrantedTransferRoleZeroAddress() public { bytes32 role = keccak256("TRANSFER_ROLE"); vm.expectEmit(true, true, true, false); - emit RoleGranted(role, address(0), 0xf29D12e4c9d593D2887EEDDe076bBe39EDf3cD0F); + emit RoleGranted(role, address(0), factory); deployContractProxy( "DropERC721", abi.encodeCall( @@ -260,7 +260,7 @@ contract DropERC721Test_initializer is BaseTest { function test_event_RoleGrantedMetadataRole() public { bytes32 role = keccak256("METADATA_ROLE"); vm.expectEmit(true, true, true, false); - emit RoleGranted(role, deployer, 0xf29D12e4c9d593D2887EEDDe076bBe39EDf3cD0F); + emit RoleGranted(role, deployer, factory); deployContractProxy( "DropERC721", abi.encodeCall( diff --git a/src/test/mocks/MockERC1155NonBurnable.sol b/src/test/mocks/MockERC1155NonBurnable.sol new file mode 100644 index 000000000..0e96fda38 --- /dev/null +++ b/src/test/mocks/MockERC1155NonBurnable.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; + +contract MockERC1155NonBurnable is ERC1155 { + constructor() ERC1155("ipfs://BaseURI") {} + + function mint( + address to, + uint256 id, + uint256 amount + ) public virtual { + _mint(to, id, amount, ""); + } + + function mintBatch( + address to, + uint256[] memory ids, + uint256[] memory amounts + ) public virtual { + _mintBatch(to, ids, amounts, ""); + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return super.supportsInterface(interfaceId); + } +} diff --git a/src/test/mocks/MockERC721NonBurnable.sol b/src/test/mocks/MockERC721NonBurnable.sol new file mode 100644 index 000000000..4668d2e75 --- /dev/null +++ b/src/test/mocks/MockERC721NonBurnable.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +contract MockERC721NonBurnable is ERC721 { + uint256 public nextTokenIdToMint; + + constructor() ERC721("MockERC721", "MOCK") {} + + function mint(address _receiver, uint256 _amount) external { + uint256 tokenId = nextTokenIdToMint; + nextTokenIdToMint += _amount; + + for (uint256 i = 0; i < _amount; i += 1) { + _mint(_receiver, tokenId); + tokenId += 1; + } + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return super.supportsInterface(interfaceId); + } +} diff --git a/src/test/scripts/generateRoot.ts b/src/test/scripts/generateRoot.ts index 9fbcbf8d4..8c72fbacf 100644 --- a/src/test/scripts/generateRoot.ts +++ b/src/test/scripts/generateRoot.ts @@ -6,8 +6,8 @@ const process = require("process"); const members = [ "0x92Bb439374a091c7507bE100183d8D1Ed2c9dAD3", - "0xD0d82c095d184e6E2c8B72689c9171DE59FFd28d", - "0xFD78F7E2dF2B8c3D5bff0413c96f3237500898B3", + "0xDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", ]; let val = process.argv[2]; diff --git a/src/test/scripts/getProof.ts b/src/test/scripts/getProof.ts index b1e41d4fd..d6c3b96a4 100644 --- a/src/test/scripts/getProof.ts +++ b/src/test/scripts/getProof.ts @@ -6,8 +6,8 @@ const process = require("process"); const members = [ "0x92Bb439374a091c7507bE100183d8D1Ed2c9dAD3", - "0xD0d82c095d184e6E2c8B72689c9171DE59FFd28d", - "0xFD78F7E2dF2B8c3D5bff0413c96f3237500898B3", + "0xDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", ]; let val = process.argv[2]; diff --git a/src/test/sdk/extension/ExtensionUtilTest.sol b/src/test/sdk/extension/ExtensionUtilTest.sol new file mode 100644 index 000000000..73d810b5e --- /dev/null +++ b/src/test/sdk/extension/ExtensionUtilTest.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; +import "../../utils/Wallet.sol"; +import "../../mocks/WETH9.sol"; +import "../../mocks/MockERC20.sol"; +import "../../mocks/MockERC721.sol"; +import "../../mocks/MockERC1155.sol"; +import { MockERC721NonBurnable } from "../../mocks/MockERC721NonBurnable.sol"; +import { MockERC1155NonBurnable } from "../../mocks/MockERC1155NonBurnable.sol"; +import "contracts/infra/forwarder/Forwarder.sol"; +import "contracts/lib/TWStrings.sol"; + +abstract contract ExtensionUtilTest is DSTest, Test { + string public constant NAME = "NAME"; + string public constant SYMBOL = "SYMBOL"; + string public constant CONTRACT_URI = "CONTRACT_URI"; + address public constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + MockERC20 public erc20; + MockERC721 public erc721; + MockERC1155 public erc1155; + MockERC721NonBurnable public erc721NonBurnable; + MockERC1155NonBurnable public erc1155NonBurnable; + WETH9 public weth; + + address public forwarder; + + address public deployer = address(0x20000); + address public saleRecipient = address(0x30000); + address public royaltyRecipient = address(0x30001); + address public platformFeeRecipient = address(0x30002); + uint128 public royaltyBps = 500; // 5% + uint128 public platformFeeBps = 500; // 5% + uint256 public constant MAX_BPS = 10_000; // 100% + + uint256 public privateKey = 1234; + address public signer; + + mapping(bytes32 => address) public contracts; + + function setUp() public virtual { + signer = vm.addr(privateKey); + + erc20 = new MockERC20(); + erc721 = new MockERC721(); + erc1155 = new MockERC1155(); + erc721NonBurnable = new MockERC721NonBurnable(); + erc1155NonBurnable = new MockERC1155NonBurnable(); + weth = new WETH9(); + forwarder = address(new Forwarder()); + } + + function getActor(uint160 _index) public pure returns (address) { + return address(uint160(0x50000 + _index)); + } + + function getWallet() public returns (Wallet wallet) { + wallet = new Wallet(); + } + + function assertIsOwnerERC721( + address _token, + address _owner, + uint256[] memory _tokenIds + ) internal { + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + bool isOwnerOfToken = MockERC721(_token).ownerOf(_tokenIds[i]) == _owner; + assertTrue(isOwnerOfToken); + } + } + + function assertIsNotOwnerERC721( + address _token, + address _owner, + uint256[] memory _tokenIds + ) internal { + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + bool isOwnerOfToken = MockERC721(_token).ownerOf(_tokenIds[i]) == _owner; + assertTrue(!isOwnerOfToken); + } + } + + function assertBalERC1155Eq( + address _token, + address _owner, + uint256[] memory _tokenIds, + uint256[] memory _amounts + ) internal { + require(_tokenIds.length == _amounts.length, "unequal lengths"); + + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + assertEq(MockERC1155(_token).balanceOf(_owner, _tokenIds[i]), _amounts[i]); + } + } + + function assertBalERC1155Gte( + address _token, + address _owner, + uint256[] memory _tokenIds, + uint256[] memory _amounts + ) internal { + require(_tokenIds.length == _amounts.length, "unequal lengths"); + + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + assertTrue(MockERC1155(_token).balanceOf(_owner, _tokenIds[i]) >= _amounts[i]); + } + } + + function assertBalERC20Eq( + address _token, + address _owner, + uint256 _amount + ) internal { + assertEq(MockERC20(_token).balanceOf(_owner), _amount); + } + + function assertBalERC20Gte( + address _token, + address _owner, + uint256 _amount + ) internal { + assertTrue(MockERC20(_token).balanceOf(_owner) >= _amount); + } + + function forwarders() public view returns (address[] memory) { + address[] memory _forwarders = new address[](1); + _forwarders[0] = forwarder; + return _forwarders; + } +} diff --git a/src/test/sdk/extension/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.t.sol b/src/test/sdk/extension/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.t.sol new file mode 100644 index 000000000..70a5b4493 --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/BatchMintMetadata.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadata is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function getBaseURI(uint256 _tokenId) public view returns (string memory) { + return _getBaseURI(_tokenId); + } +} + +contract BatchMintMetadata_BatchMintMetadata is ExtensionUtilTest { + MyBatchMintMetadata internal ext; + uint256 internal startId; + uint256 internal amountToMint; + string internal baseURI; + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadata(); + startId = 0; + amountToMint = 100; + baseURI = "ipfs://baseURI"; + } + + function test_batchMintMetadata() public { + uint256 prevBaseURICount = ext.getBaseURICount(); + uint256 batchId = startId + amountToMint; + + ext.batchMintMetadata(startId, amountToMint, baseURI); + uint256 newBaseURICount = ext.getBaseURICount(); + assertEq(ext.getBaseURI(amountToMint - 1), baseURI); + assertEq(newBaseURICount, prevBaseURICount + 1); + assertEq(ext.getBatchIdAtIndex(newBaseURICount - 1), batchId); + + vm.expectRevert("Invalid index"); + ext.getBatchIdAtIndex(newBaseURICount); + } +} diff --git a/src/test/sdk/extension/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.tree b/src/test/sdk/extension/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.tree new file mode 100644 index 000000000..572dd5203 --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.tree @@ -0,0 +1,7 @@ +_batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens +) +├── it should store batch id equal to the sum of `_startId` and `_amountToMint` in batchIds array ✅ +├── it should map the new batch id to `_baseURIForTokens` ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.t.sol b/src/test/sdk/extension/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.t.sol new file mode 100644 index 000000000..ac839fbcd --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.t.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/BatchMintMetadata.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadata is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function freezeBaseURI(uint256 _batchId) external { + _freezeBaseURI(_batchId); + } +} + +contract BatchMintMetadata_FreezeBaseURI is ExtensionUtilTest { + MyBatchMintMetadata internal ext; + uint256 internal startId; + uint256[] internal batchIds; + uint256 internal indexToFreeze; + + event MetadataFrozen(); + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadata(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + uint256 batchId = startId + amount; + batchIds.push(batchId); + + (startId, ) = ext.batchMintMetadata(startId, amount, "ipfs://"); + assertEq(ext.batchFrozen(batchId), false); + } + + indexToFreeze = 3; + } + + function test_freezeBaseURI_invalidBatch() public { + vm.expectRevert("Invalid batch"); + ext.freezeBaseURI(batchIds[indexToFreeze] * 10); // non-existent batchId + } + + modifier whenBatchIdValid() { + _; + } + + function test_freezeBaseURI() public whenBatchIdValid { + ext.freezeBaseURI(batchIds[indexToFreeze]); + + assertEq(ext.batchFrozen(batchIds[indexToFreeze]), true); + } + + function test_freezeBaseURI_event() public whenBatchIdValid { + vm.expectEmit(false, false, false, false); + emit MetadataFrozen(); + ext.freezeBaseURI(batchIds[indexToFreeze]); + } +} diff --git a/src/test/sdk/extension/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.tree b/src/test/sdk/extension/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.tree new file mode 100644 index 000000000..4dd87edef --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.tree @@ -0,0 +1,6 @@ +_freezeBaseURI(uint256 _batchId) +├── when there is no baseURI for given `_batchId` + │ └── it should revert ✅ + └── when there is a baseURI present for given `_batchId` + └── it should freeze the `batchId` by setting `frozen[_batchId]` to `true` ✅ + └── it should emit MetadataFrozen event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/batch-mint-metadata/get-base-uri/_getBaseURI.t.sol b/src/test/sdk/extension/batch-mint-metadata/get-base-uri/_getBaseURI.t.sol new file mode 100644 index 000000000..088adba26 --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/get-base-uri/_getBaseURI.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/BatchMintMetadata.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadata is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function getBaseURI(uint256 _tokenId) external view returns (string memory) { + return _getBaseURI(_tokenId); + } +} + +contract BatchMintMetadata_GetBaseURI is ExtensionUtilTest { + MyBatchMintMetadata internal ext; + uint256 internal startId; + uint256[] internal batchIds; + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadata(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + uint256 batchId = startId + amount; + batchIds.push(batchId); + + string memory baseURI = Strings.toString(batchId); + (startId, ) = ext.batchMintMetadata(startId, amount, baseURI); + } + } + + function test_getBaseURI_invalidTokenId() public { + uint256 tokenId = batchIds[4]; // tokenId greater than the last batchId + + vm.expectRevert("Invalid tokenId"); + ext.getBaseURI(tokenId); + } + + modifier whenValidTokenId() { + _; + } + + function test_getBaseURI() public whenValidTokenId { + for (uint256 i = 0; i < 5; i++) { + uint256 start = i == 0 ? 0 : batchIds[i - 1]; + for (uint256 j = start; j < batchIds[i]; j++) { + string memory _baseURI = ext.getBaseURI(j); + + assertEq(_baseURI, Strings.toString(batchIds[i])); + } + } + } +} diff --git a/src/test/sdk/extension/batch-mint-metadata/get-base-uri/_getBaseURI.tree b/src/test/sdk/extension/batch-mint-metadata/get-base-uri/_getBaseURI.tree new file mode 100644 index 000000000..c4ee674bf --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/get-base-uri/_getBaseURI.tree @@ -0,0 +1,6 @@ +_getBaseURI(uint256 _tokenId) +├── when `_tokenId` doesn't belong to any batch, i.e. greater than the last batchId + │ └── it should revert ✅ + └── when `_tokenId` belongs to some batch, i.e. less than that batchId + └── it should return correct baseURI for the `_tokenId` ✅ +(note: all batches are assumed to be contiguous, i.e. start id of one batch is the end id of the previous batch) \ No newline at end of file diff --git a/src/test/sdk/extension/batch-mint-metadata/get-batch-id/_getBatchId.t.sol b/src/test/sdk/extension/batch-mint-metadata/get-batch-id/_getBatchId.t.sol new file mode 100644 index 000000000..ffea5de91 --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/get-batch-id/_getBatchId.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/BatchMintMetadata.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadata is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function getBatchId(uint256 _tokenId) external view returns (uint256 batchId, uint256 index) { + return _getBatchId(_tokenId); + } +} + +contract BatchMintMetadata_GetBatchId is ExtensionUtilTest { + MyBatchMintMetadata internal ext; + uint256 internal startId; + uint256[] internal batchIds; + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadata(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + batchIds.push(startId + amount); + (startId, ) = ext.batchMintMetadata(startId, amount, "ipfs://"); + } + } + + function test_getBatchId_invalidTokenId() public { + uint256 tokenId = batchIds[4]; // tokenId greater than the last batchId + + vm.expectRevert("Invalid tokenId"); + ext.getBatchId(tokenId); + } + + modifier whenValidTokenId() { + _; + } + + function test_getBatchId() public whenValidTokenId { + for (uint256 i = 0; i < 5; i++) { + uint256 start = i == 0 ? 0 : batchIds[i - 1]; + for (uint256 j = start; j < batchIds[i]; j++) { + (uint256 batchId, uint256 index) = ext.getBatchId(j); + + assertEq(batchId, batchIds[i]); + assertEq(index, i); + } + } + } +} diff --git a/src/test/sdk/extension/batch-mint-metadata/get-batch-id/_getBatchId.tree b/src/test/sdk/extension/batch-mint-metadata/get-batch-id/_getBatchId.tree new file mode 100644 index 000000000..2e6dd366e --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/get-batch-id/_getBatchId.tree @@ -0,0 +1,6 @@ +_getBatchId(uint256 _tokenId) +├── when `_tokenId` doesn't belong to any batch, i.e. greater than the last batchId + │ └── it should revert ✅ + └── when `_tokenId` belongs to some batch, i.e. less than that batchId + └── it should return correct batchId and batch index for the `_tokenId` ✅ +(note: all batches are assumed to be contiguous, i.e. start id of one batch is the end id of the previous batch) \ No newline at end of file diff --git a/src/test/sdk/extension/batch-mint-metadata/get-batch-start-id/_getBatchStartId.t.sol b/src/test/sdk/extension/batch-mint-metadata/get-batch-start-id/_getBatchStartId.t.sol new file mode 100644 index 000000000..b3cfba004 --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/get-batch-start-id/_getBatchStartId.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/BatchMintMetadata.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadata is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function getBatchStartId(uint256 _batchId) external view returns (uint256) { + return _getBatchStartId(_batchId); + } +} + +contract BatchMintMetadata_GetBatchStartId is ExtensionUtilTest { + MyBatchMintMetadata internal ext; + uint256 internal startId; + uint256[] internal batchIds; + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadata(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + batchIds.push(startId + amount); + (startId, ) = ext.batchMintMetadata(startId, amount, "ipfs://"); + } + } + + function test_getBatchStartId_invalidBatchId() public { + uint256 batchId = batchIds[4] + 1; // non-existent batchId + + vm.expectRevert("Invalid batchId"); + ext.getBatchStartId(batchId); + } + + modifier whenValidBatchId() { + _; + } + + function test_getBatchStartId() public whenValidBatchId { + for (uint256 i = 0; i < 5; i++) { + uint256 start = i == 0 ? 0 : batchIds[i - 1]; + uint256 _batchStartId = ext.getBatchStartId(batchIds[i]); + + assertEq(start, _batchStartId); + } + } +} diff --git a/src/test/sdk/extension/batch-mint-metadata/get-batch-start-id/_getBatchStartId.tree b/src/test/sdk/extension/batch-mint-metadata/get-batch-start-id/_getBatchStartId.tree new file mode 100644 index 000000000..7e303ab46 --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/get-batch-start-id/_getBatchStartId.tree @@ -0,0 +1,6 @@ +_getBatchStartId(uint256 _batchID) +├── when `_batchID` doesn't exist + │ └── it should revert ✅ + └── when `_batchID` exists + └── it should return the starting tokenId for that batch ✅ +(note: all batches are assumed to be contiguous, i.e. start id of one batch is the end id of the previous batch) \ No newline at end of file diff --git a/src/test/sdk/extension/batch-mint-metadata/set-base-uri/_setBaseURI.t.sol b/src/test/sdk/extension/batch-mint-metadata/set-base-uri/_setBaseURI.t.sol new file mode 100644 index 000000000..ee4baa4d6 --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/set-base-uri/_setBaseURI.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/BatchMintMetadata.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadata is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function setBaseURI(uint256 _batchId, string memory _baseURI) external { + _setBaseURI(_batchId, _baseURI); + } + + function freezeBaseURI(uint256 _batchId, bool _freeze) public { + batchFrozen[_batchId] = _freeze; + } + + function getBaseURI(uint256 _tokenId) external view returns (string memory) { + return _getBaseURI(_tokenId); + } +} + +contract BatchMintMetadata_SetBaseURI is ExtensionUtilTest { + MyBatchMintMetadata internal ext; + string internal newBaseURI; + uint256 internal startId; + uint256[] internal batchIds; + uint256 internal indexToUpdate; + + event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadata(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + uint256 batchId = startId + amount; + batchIds.push(batchId); + + (startId, ) = ext.batchMintMetadata(startId, amount, "ipfs://"); + ext.freezeBaseURI(batchId, true); + } + + indexToUpdate = 3; + newBaseURI = "ipfs://baseURI"; + } + + function test_setBaseURI_frozenBatchId() public { + vm.expectRevert("Batch frozen"); + ext.setBaseURI(batchIds[indexToUpdate], newBaseURI); + } + + modifier whenBatchIdNotFrozen() { + ext.freezeBaseURI(batchIds[indexToUpdate], false); + _; + } + + function test_setBaseURI() public whenBatchIdNotFrozen { + ext.setBaseURI(batchIds[indexToUpdate], newBaseURI); + + string memory _baseURI = ext.getBaseURI(batchIds[indexToUpdate] - 1); + + assertEq(_baseURI, newBaseURI); + } + + function test_setBaseURI_event() public whenBatchIdNotFrozen { + vm.expectEmit(false, false, false, true); + emit BatchMetadataUpdate(batchIds[indexToUpdate - 1], batchIds[indexToUpdate]); + ext.setBaseURI(batchIds[indexToUpdate], newBaseURI); + } +} diff --git a/src/test/sdk/extension/batch-mint-metadata/set-base-uri/_setBaseURI.tree b/src/test/sdk/extension/batch-mint-metadata/set-base-uri/_setBaseURI.tree new file mode 100644 index 000000000..3df76f653 --- /dev/null +++ b/src/test/sdk/extension/batch-mint-metadata/set-base-uri/_setBaseURI.tree @@ -0,0 +1,6 @@ +_setBaseURI(uint256 _batchId, string memory _baseURI) +├── when the `_batchId` is frozen + │ └── it should revert ✅ + └── when the `_batchId` is not frozen + └── it should map the `_batchId` to `_baseURI` param ✅ + └── it should emit BatchMetadataUpdate event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.t.sol b/src/test/sdk/extension/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.t.sol new file mode 100644 index 000000000..c03db391c --- /dev/null +++ b/src/test/sdk/extension/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.t.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BurnToClaim, IBurnToClaim } from "contracts/extension/BurnToClaim.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyBurnToClaim is BurnToClaim { + function burnTokensOnOrigin( + address _tokenOwner, + uint256 _tokenId, + uint256 _quantity + ) public { + _burnTokensOnOrigin(_tokenOwner, _tokenId, _quantity); + } + + function _canSetBurnToClaim() internal view override returns (bool) { + return true; + } +} + +contract BurnToClaim_BurnTokensOnOrigin is ExtensionUtilTest { + MyBurnToClaim internal ext; + Wallet internal tokenOwner; + uint256 internal tokenId; + uint256 internal quantity; + + function setUp() public override { + super.setUp(); + + ext = new MyBurnToClaim(); + + tokenOwner = getWallet(); + erc721.mint(address(tokenOwner), 10); + erc1155.mint(address(tokenOwner), 1, 10); + + erc721NonBurnable.mint(address(tokenOwner), 10); + erc1155NonBurnable.mint(address(tokenOwner), 1, 10); + + tokenOwner.setApprovalForAllERC721(address(erc721), address(ext), true); + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(ext), true); + } + + // ================== + // ======= Test branch: token type is ERC721 + // ================== + + modifier whenNotBurnableERC721() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721NonBurnable), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + _; + } + + function test_burnTokensOnOrigin_ERC721_nonBurnable() public whenNotBurnableERC721 { + vm.expectRevert(); + ext.burnTokensOnOrigin(address(tokenOwner), tokenId, quantity); + } + + modifier whenBurnableERC721() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + _; + } + + function test_burnTokensOnOrigin_ERC721() public whenBurnableERC721 { + ext.burnTokensOnOrigin(address(tokenOwner), tokenId, quantity); + + assertEq(erc721.balanceOf(address(tokenOwner)), 9); + + vm.expectRevert(); + erc721.ownerOf(tokenId); // token doesn't exist after burning + } + + // ================== + // ======= Test branch: token type is ERC71155 + // ================== + + modifier whenNotBurnableERC1155() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155NonBurnable), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 1, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + _; + } + + function test_burnTokensOnOrigin_ERC1155_nonBurnable() public whenNotBurnableERC1155 { + vm.expectRevert(); + ext.burnTokensOnOrigin(address(tokenOwner), tokenId, quantity); + } + + modifier whenBurnableERC1155() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 1, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + _; + } + + function test_burnTokensOnOrigin_ERC1155() public whenBurnableERC1155 { + ext.burnTokensOnOrigin(address(tokenOwner), tokenId, quantity); + + assertEq(erc1155.balanceOf(address(tokenOwner), tokenId), 0); + } +} diff --git a/src/test/sdk/extension/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.tree b/src/test/sdk/extension/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.tree new file mode 100644 index 000000000..a2a3911ac --- /dev/null +++ b/src/test/sdk/extension/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.tree @@ -0,0 +1,15 @@ +_burnTokensOnOrigin( + address _tokenOwner, + uint256 _tokenId, + uint256 _quantity +) +├── when burn-to-claim info has token type ERC721 + ├── when the origin ERC721 contract is not burnable + │ └── it should revert ✅ + └── when the origin ERC721 contract is burnable + └── it should successfully burn the token with given tokenId for the token owner ✅ +├── when burn-to-claim info has token type ERC1155 + ├── when the origin ERC1155 contract is not burnable + │ └── it should revert ✅ + └── when the origin ERC1155 contract is burnable + └── it should successfully burn tokens with given tokenId and quantity for the token owner ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.t.sol b/src/test/sdk/extension/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.t.sol new file mode 100644 index 000000000..b4e721145 --- /dev/null +++ b/src/test/sdk/extension/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BurnToClaim, IBurnToClaim } from "contracts/extension/BurnToClaim.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyBurnToClaim is BurnToClaim { + bool condition; + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _canSetBurnToClaim() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract BurnToClaim_SetBurnToClaimInfo is ExtensionUtilTest { + MyBurnToClaim internal ext; + address internal admin; + address internal caller; + IBurnToClaim.BurnToClaimInfo internal info; + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + + ext = new MyBurnToClaim(address(admin)); + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(0), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(0) + }); + } + + function test_setBurnToClaimInfo_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized."); + ext.setBurnToClaimInfo(info); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setBurnToClaimInfo_invalidOriginContract_addressZero() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectRevert("Origin contract not set."); + ext.setBurnToClaimInfo(info); + } + + modifier whenValidOriginContract() { + info.originContractAddress = address(erc721); + _; + } + + function test_setBurnToClaimInfo_invalidCurrency_addressZero() public whenCallerAuthorized whenValidOriginContract { + vm.prank(address(caller)); + vm.expectRevert("Currency not set."); + ext.setBurnToClaimInfo(info); + } + + modifier whenValidCurrency() { + info.currency = address(erc20); + _; + } + + function test_setBurnToClaimInfo() public whenCallerAuthorized whenValidOriginContract whenValidCurrency { + vm.prank(address(caller)); + ext.setBurnToClaimInfo(info); + + IBurnToClaim.BurnToClaimInfo memory _info = ext.getBurnToClaimInfo(); + + assertEq(_info.originContractAddress, info.originContractAddress); + assertEq(_info.currency, info.currency); + } +} diff --git a/src/test/sdk/extension/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.tree b/src/test/sdk/extension/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.tree new file mode 100644 index 000000000..d6e347f5e --- /dev/null +++ b/src/test/sdk/extension/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.tree @@ -0,0 +1,11 @@ +setBurnToClaimInfo(BurnToClaimInfo calldata _burnToClaimInfo) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when input originContractAddress is address(0) + │ └── it should revert ✅ + └── when input originContractAddress is not address(0) + ├── when input currency is address(0) + │ └── it should revert ✅ + └── when input currency is not address(0) + └── it should save incoming struct values into burnToClaimInfo state ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.t.sol b/src/test/sdk/extension/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.t.sol new file mode 100644 index 000000000..b985d473d --- /dev/null +++ b/src/test/sdk/extension/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.t.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BurnToClaim, IBurnToClaim } from "contracts/extension/BurnToClaim.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyBurnToClaim is BurnToClaim { + bool condition; + + function setCondition(bool _condition) external { + condition = _condition; + } + + function _canSetBurnToClaim() internal view override returns (bool) { + return condition; + } +} + +contract BurnToClaim_VerifyBurnToClaim is ExtensionUtilTest { + MyBurnToClaim internal ext; + address internal tokenOwner; + uint256 internal tokenId; + uint256 internal quantity; + + function setUp() public override { + super.setUp(); + + ext = new MyBurnToClaim(); + ext.setCondition(true); + + tokenOwner = getActor(1); + erc721.mint(address(tokenOwner), 10); + erc1155.mint(address(tokenOwner), 1, 10); + } + + function test_verifyBurnToClaim_infoNotSet() public { + vm.expectRevert(); + ext.verifyBurnToClaim(tokenOwner, tokenId, 1); + } + + // ================== + // ======= Test branch: token type is ERC721 + // ================== + + modifier whenBurnToClaimInfoSetERC721() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + IBurnToClaim.BurnToClaimInfo memory info = ext.getBurnToClaimInfo(); + _; + } + + function test_verifyBurnToClaim_ERC721_quantity_not_1() public whenBurnToClaimInfoSetERC721 { + quantity = 10; + vm.expectRevert("Invalid amount"); + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); + } + + modifier whenQuantityParamisOne() { + quantity = 1; + _; + } + + function test_verifyBurnToClaim_ERC721_notOwnerOfToken() + public + whenBurnToClaimInfoSetERC721 + whenQuantityParamisOne + { + vm.expectRevert("!Owner"); + ext.verifyBurnToClaim(address(0x123), tokenId, quantity); // random address as owner + } + + modifier whenCorrectOwner() { + _; + } + + function test_verifyBurnToClaim_ERC721() + public + whenBurnToClaimInfoSetERC721 + whenQuantityParamisOne + whenCorrectOwner + { + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); + } + + // ================== + // ======= Test branch: token type is ERC1155 + // ================== + + modifier whenBurnToClaimInfoSetERC1155() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 1, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + IBurnToClaim.BurnToClaimInfo memory info = ext.getBurnToClaimInfo(); + _; + } + + function test_verifyBurnToClaim_ERC1155_invalidTokenId() public whenBurnToClaimInfoSetERC1155 { + vm.expectRevert("Invalid token Id"); + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); // the tokenId here is 0, but eligible one is set as 1 above + } + + modifier whenCorrectTokenId() { + tokenId = 1; + _; + } + + function test_verifyBurnToClaim_ERC1155_balanceLessThanQuantity() + public + whenBurnToClaimInfoSetERC1155 + whenCorrectTokenId + { + quantity = 100; + vm.expectRevert("!Balance"); + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); // available balance is 10 + } + + modifier whenSufficientBalance() { + quantity = 10; + _; + } + + function test_verifyBurnToClaim_ERC1155() + public + whenBurnToClaimInfoSetERC1155 + whenCorrectTokenId + whenSufficientBalance + { + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); + } +} diff --git a/src/test/sdk/extension/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.tree b/src/test/sdk/extension/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.tree new file mode 100644 index 000000000..ffc4dba9d --- /dev/null +++ b/src/test/sdk/extension/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.tree @@ -0,0 +1,23 @@ +verifyBurnToClaim( + address tokenOwner, + uint256 tokenId, + uint256 quantity +) +├── when burn-to-claim info is not set + │ └── it should revert ✅ + └── when burn-to-claim info is set, with token type ERC721 + │ ├── when quantity param is not 1 + │ │ └── it should revert ✅ + │ └── when quantity param is 1 + │ ├── when token owner param is not the actual token owner + │ │ └── it should revert ✅ + │ └── when token owner param is the correct token owner + │ │ └── execution completes -- exit function ✅ + └── when burn-to-claim info is set, with token type ERC1155 + ├── when tokenId param doesn't match eligible tokenId + │ └── it should revert ✅ + └── when tokenId param matches eligible tokenId + ├── when token owner has balance less than quantity param + │ └── it should revert ✅ + └── when token owner has balance greater than or equal to quantity param + └── execution completes -- exit function ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/contract-metadata/set-contract-uri/setContractURI.t.sol b/src/test/sdk/extension/contract-metadata/set-contract-uri/setContractURI.t.sol new file mode 100644 index 000000000..30b463c4d --- /dev/null +++ b/src/test/sdk/extension/contract-metadata/set-contract-uri/setContractURI.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { ContractMetadata, IContractMetadata } from "contracts/extension/ContractMetadata.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyContractMetadata is ContractMetadata { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _canSetContractURI() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract ContractMetadata_SetContractURI is ExtensionUtilTest { + MyContractMetadata internal ext; + address internal admin; + address internal caller; + string internal uri; + + event ContractURIUpdated(string prevURI, string newURI); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + uri = "ipfs://newUri"; + + ext = new MyContractMetadata(address(admin)); + } + + function test_setContractURI_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + ext.setContractURI(uri); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setContractURI() public whenCallerAuthorized { + vm.prank(address(caller)); + ext.setContractURI(uri); + + string memory _updatedUri = ext.contractURI(); + assertEq(_updatedUri, uri); + } + + function test_setContractURI_event() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectEmit(false, false, false, true); + emit ContractURIUpdated("", uri); + ext.setContractURI(uri); + } +} diff --git a/src/test/sdk/extension/contract-metadata/set-contract-uri/setContractURI.tree b/src/test/sdk/extension/contract-metadata/set-contract-uri/setContractURI.tree new file mode 100644 index 000000000..e626d76e4 --- /dev/null +++ b/src/test/sdk/extension/contract-metadata/set-contract-uri/setContractURI.tree @@ -0,0 +1,6 @@ +setContractURI(string memory uri) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── it should update contract URI to the new URI value ✅ + └── it should emit ContractURIUpdated event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/delayed-reveal/get-reveal-uri/getRevealURI.t.sol b/src/test/sdk/extension/delayed-reveal/get-reveal-uri/getRevealURI.t.sol new file mode 100644 index 000000000..ad9e08828 --- /dev/null +++ b/src/test/sdk/extension/delayed-reveal/get-reveal-uri/getRevealURI.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { DelayedReveal, IDelayedReveal } from "contracts/extension/DelayedReveal.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyDelayedReveal is DelayedReveal { + function setEncryptedData(uint256 _batchId, bytes memory _encryptedData) external { + _setEncryptedData(_batchId, _encryptedData); + } + + function reveal(uint256 identifier, bytes calldata key) external returns (string memory revealedURI) {} +} + +contract DelayedReveal_GetRevealURI is ExtensionUtilTest { + MyDelayedReveal internal ext; + string internal originalURI; + bytes internal encryptionKey; + bytes internal encryptedURI; + bytes internal encryptedData; + uint256 internal batchId; + bytes32 internal provenanceHash; + + function setUp() public override { + super.setUp(); + + ext = new MyDelayedReveal(); + originalURI = "ipfs://original"; + encryptionKey = "key123"; + batchId = 1; + + provenanceHash = keccak256(abi.encodePacked(originalURI, encryptionKey, block.chainid)); + encryptedURI = ext.encryptDecrypt(bytes(originalURI), encryptionKey); + encryptedData = abi.encode(encryptedURI, provenanceHash); + } + + function test_getRevealURI_encryptedDataNotSet() public { + vm.expectRevert("Nothing to reveal"); + ext.getRevealURI(batchId, encryptionKey); + } + + modifier whenEncryptedDataIsSet() { + ext.setEncryptedData(batchId, encryptedData); + _; + } + + function test_getRevealURI_incorrectKey() public whenEncryptedDataIsSet { + bytes memory incorrectKey = "incorrect key"; + + vm.expectRevert("Incorrect key"); + ext.getRevealURI(batchId, incorrectKey); + } + + modifier whenCorrectKey() { + _; + } + + function test_getRevealURI() public whenEncryptedDataIsSet whenCorrectKey { + string memory revealedURI = ext.getRevealURI(batchId, encryptionKey); + + assertEq(originalURI, revealedURI); + } +} diff --git a/src/test/sdk/extension/delayed-reveal/get-reveal-uri/getRevealURI.tree b/src/test/sdk/extension/delayed-reveal/get-reveal-uri/getRevealURI.tree new file mode 100644 index 000000000..acb580468 --- /dev/null +++ b/src/test/sdk/extension/delayed-reveal/get-reveal-uri/getRevealURI.tree @@ -0,0 +1,8 @@ +getRevealURI(uint256 _batchId, bytes calldata _key) +├── when there is no encrypted data set for the given batch id + │ └── it should revert ✅ + └── when there is an associated encrypted data present for the given batch id + ├── when the encryption key provided is incorrect + │ └── it should revert ✅ + └── when the encryption key provided is correct + └── it should correctly decrypt and return the original URI ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/delayed-reveal/set-encrypted-data/_setEncryptedData.t.sol b/src/test/sdk/extension/delayed-reveal/set-encrypted-data/_setEncryptedData.t.sol new file mode 100644 index 000000000..096e33568 --- /dev/null +++ b/src/test/sdk/extension/delayed-reveal/set-encrypted-data/_setEncryptedData.t.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { DelayedReveal, IDelayedReveal } from "contracts/extension/DelayedReveal.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyDelayedReveal is DelayedReveal { + function setEncryptedData(uint256 _batchId, bytes memory _encryptedData) external { + _setEncryptedData(_batchId, _encryptedData); + } + + function reveal(uint256 identifier, bytes calldata key) external returns (string memory revealedURI) {} +} + +contract DelayedReveal_SetEncryptedData is ExtensionUtilTest { + MyDelayedReveal internal ext; + uint256 internal batchId; + bytes internal data; + + function setUp() public override { + super.setUp(); + + ext = new MyDelayedReveal(); + batchId = 1; + data = "test"; + } + + function test_setEncryptedData() public { + ext.setEncryptedData(batchId, data); + + assertEq(true, ext.isEncryptedBatch(batchId)); + assertEq(ext.encryptedData(batchId), data); + } +} diff --git a/src/test/sdk/extension/delayed-reveal/set-encrypted-data/_setEncryptedData.tree b/src/test/sdk/extension/delayed-reveal/set-encrypted-data/_setEncryptedData.tree new file mode 100644 index 000000000..68f99a2c8 --- /dev/null +++ b/src/test/sdk/extension/delayed-reveal/set-encrypted-data/_setEncryptedData.tree @@ -0,0 +1,3 @@ +_setEncryptedData(uint256 _batchId, bytes memory _encryptedData) +├── it should store input bytes data for the given batch id param ✅ +├── isEncryptedBatch should return true for this batch id ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/drop/claim/claim.t.sol b/src/test/sdk/extension/drop/claim/claim.t.sol new file mode 100644 index 000000000..ecf50fede --- /dev/null +++ b/src/test/sdk/extension/drop/claim/claim.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Drop, IDrop, IClaimConditionMultiPhase, IClaimCondition } from "contracts/extension/Drop.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyDrop is Drop { + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override {} + + function _transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) + internal + override + returns (uint256 startTokenId) + {} + + function _canSetClaimConditions() internal view override returns (bool) { + return true; + } + + function verifyClaim( + uint256 _conditionId, + address _claimer, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof + ) public view override returns (bool isOverride) {} +} + +contract Drop_Claim is ExtensionUtilTest { + MyDrop internal ext; + + address internal _claimer; + uint256 internal _quantity; + address internal _currency; + uint256 internal _pricePerToken; + IDrop.AllowlistProof internal _allowlistProof; + + IClaimCondition.ClaimCondition[] internal claimConditions; + + function setUp() public override { + super.setUp(); + + ext = new MyDrop(); + _claimer = getActor(1); + _quantity = 10; + } + + function _setConditionsState() public { + // values here are not important (except timestamp), since we won't be verifying claim params + + claimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 0, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + ext.setClaimConditions(claimConditions, false); + } + + function test_claim_noConditionsSet() public { + vm.expectRevert("!CONDITION."); + ext.claim(_claimer, _quantity, _currency, _pricePerToken, _allowlistProof, ""); + } + + modifier whenConditionsAreSet() { + _setConditionsState(); + _; + } + + function test_claim() public whenConditionsAreSet { + // claim + vm.prank(_claimer); + ext.claim(_claimer, _quantity, _currency, _pricePerToken, _allowlistProof, ""); + + uint256 supplyClaimedByWallet_1 = ext.getSupplyClaimedByWallet(0, _claimer); + uint256 supplyClaimed_1 = (ext.getClaimConditionById(0)).supplyClaimed; + + // claim again + vm.prank(_claimer); + ext.claim(_claimer, _quantity, _currency, _pricePerToken, _allowlistProof, ""); + + uint256 supplyClaimedByWallet_2 = ext.getSupplyClaimedByWallet(0, _claimer); + uint256 supplyClaimed_2 = (ext.getClaimConditionById(0)).supplyClaimed; + + // check state + assertEq(supplyClaimedByWallet_1, _quantity); + assertEq(supplyClaimedByWallet_2, supplyClaimedByWallet_1 + _quantity); + + assertEq(supplyClaimed_1, _quantity); + assertEq(supplyClaimed_2, supplyClaimed_1 + _quantity); + } +} diff --git a/src/test/sdk/extension/drop/claim/claim.tree b/src/test/sdk/extension/drop/claim/claim.tree new file mode 100644 index 000000000..4ca1d3187 --- /dev/null +++ b/src/test/sdk/extension/drop/claim/claim.tree @@ -0,0 +1,15 @@ +claim( + address _receiver, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data +) +├── when no active condition + │ └── it should revert ✅ + └── when there's an active condition + └── it should increase the supplyClaimed for that condition by quantity param input ✅ + └── it should increase the supplyClaimedByWallet for that condition and msg.sender by quantity param input ✅ + +(Note: verifyClaim function has been tested separately, and hence not being tested here) \ No newline at end of file diff --git a/src/test/sdk/extension/drop/get-active-claim-condition-id/getActiveClaimConditionId.t.sol b/src/test/sdk/extension/drop/get-active-claim-condition-id/getActiveClaimConditionId.t.sol new file mode 100644 index 000000000..0c74f1390 --- /dev/null +++ b/src/test/sdk/extension/drop/get-active-claim-condition-id/getActiveClaimConditionId.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Drop, IDrop, IClaimConditionMultiPhase, IClaimCondition } from "contracts/extension/upgradeable/Drop.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyDrop is Drop { + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override {} + + function _transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) + internal + override + returns (uint256 startTokenId) + {} + + function _canSetClaimConditions() internal view override returns (bool) { + return true; + } +} + +contract Drop_GetActiveClaimConditionId is ExtensionUtilTest { + MyDrop internal ext; + + IClaimCondition.ClaimCondition[] internal claimConditions; + + function setUp() public override { + super.setUp(); + + ext = new MyDrop(); + } + + function _setConditionsState() public { + claimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 100, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + claimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 200, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + claimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 300, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + ext.setClaimConditions(claimConditions, false); + } + + function test_getActiveClaimConditionId_noConditionsSet() public { + vm.expectRevert("!CONDITION."); + ext.getActiveClaimConditionId(); + } + + modifier whenConditionsAreSet() { + _setConditionsState(); + _; + } + + function test_getActiveClaimConditionId_noActiveCondition() public whenConditionsAreSet { + vm.expectRevert("!CONDITION."); + ext.getActiveClaimConditionId(); + } + + modifier whenActiveConditions() { + _; + } + + function test_getActiveClaimConditionId_activeConditions() public whenConditionsAreSet whenActiveConditions { + vm.warp(claimConditions[0].startTimestamp); + + uint256 id = ext.getActiveClaimConditionId(); + assertEq(id, 0); + + vm.warp(claimConditions[1].startTimestamp); + + id = ext.getActiveClaimConditionId(); + assertEq(id, 1); + + vm.warp(claimConditions[2].startTimestamp); + + id = ext.getActiveClaimConditionId(); + assertEq(id, 2); + } +} diff --git a/src/test/sdk/extension/drop/get-active-claim-condition-id/getActiveClaimConditionId.tree b/src/test/sdk/extension/drop/get-active-claim-condition-id/getActiveClaimConditionId.tree new file mode 100644 index 000000000..8b8a94d99 --- /dev/null +++ b/src/test/sdk/extension/drop/get-active-claim-condition-id/getActiveClaimConditionId.tree @@ -0,0 +1,8 @@ +getActiveClaimConditionId() +├── when no conditions are set + │ └── it should revert ✅ + └── when condition(s) are set + ├── when no active condition, i.e. start timestamps of all conditions greater than block timestamp + │ └── it should revert ✅ + └── when conditions active, i.e. start timestamps at least one condition is less than or equal to the block timestamp + └── it should return the latest active claim condition id (i.e. with highest start timestamp among those active) ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/drop/set-claim-conditions/setClaimConditions.t.sol b/src/test/sdk/extension/drop/set-claim-conditions/setClaimConditions.t.sol new file mode 100644 index 000000000..11bc71b16 --- /dev/null +++ b/src/test/sdk/extension/drop/set-claim-conditions/setClaimConditions.t.sol @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Drop, IDrop, IClaimConditionMultiPhase, IClaimCondition } from "contracts/extension/Drop.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyDrop is Drop { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override {} + + function _transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) + internal + override + returns (uint256 startTokenId) + {} + + function _canSetClaimConditions() internal view override returns (bool) { + return msg.sender == admin; + } + + /** + * note: the functions below are dummy functions for test purposes, + * to directly access and set/reset state without going through the actual functions and checks + */ + + function setCondition(ClaimCondition calldata condition, uint256 _conditionId) public { + claimCondition.conditions[_conditionId] = condition; + } + + function setSupplyClaimedForCondition(uint256 _conditionId, uint256 _supplyClaimed) public { + claimCondition.conditions[_conditionId].supplyClaimed = _supplyClaimed; + } +} + +contract Drop_SetClaimConditions is ExtensionUtilTest { + MyDrop internal ext; + address internal admin; + + IClaimCondition.ClaimCondition[] internal newClaimConditions; + IClaimCondition.ClaimCondition[] internal oldClaimConditions; + + event ClaimConditionsUpdated(IClaimCondition.ClaimCondition[] claimConditions, bool resetEligibility); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + ext = new MyDrop(admin); + + _setOldConditionsState(); + + newClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 100, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + newClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 200, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + } + + function _setOldConditionsState() public { + oldClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 10, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + oldClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 20, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + oldClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 30, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + vm.prank(admin); + ext.setClaimConditions(oldClaimConditions, false); + (, uint256 count) = ext.claimCondition(); + assertEq(count, oldClaimConditions.length); + + ext.setSupplyClaimedForCondition(0, 5); + ext.setSupplyClaimedForCondition(0, 20); + ext.setSupplyClaimedForCondition(0, 100); + } + + function test_setClaimConditions_notAuthorized() public { + vm.expectRevert("Not authorized"); + ext.setClaimConditions(newClaimConditions, false); + + vm.expectRevert("Not authorized"); + ext.setClaimConditions(newClaimConditions, true); + } + + modifier whenCallerAuthorized() { + vm.startPrank(admin); + _; + vm.stopPrank(); + } + + function test_setClaimConditions_incorrectStartTimestamps() public whenCallerAuthorized { + // reverse the order of timestamps + newClaimConditions[0].startTimestamp = newClaimConditions[1].startTimestamp + 100; + + vm.expectRevert(bytes("ST")); + ext.setClaimConditions(newClaimConditions, false); + + vm.expectRevert(bytes("ST")); + ext.setClaimConditions(newClaimConditions, true); + } + + modifier whenCorrectTimestamps() { + _; + } + + // ================== + // ======= Test branch: claim eligibility reset + // ================== + + function test_setClaimConditions_resetEligibility_startIndex() public whenCallerAuthorized whenCorrectTimestamps { + (, uint256 oldCount) = ext.claimCondition(); + + ext.setClaimConditions(newClaimConditions, true); + + (uint256 newStartIndex, ) = ext.claimCondition(); + assertEq(newStartIndex, oldCount); + } + + function test_setClaimConditions_resetEligibility_conditionCount() + public + whenCallerAuthorized + whenCorrectTimestamps + { + (, uint256 oldCount) = ext.claimCondition(); + assertEq(oldCount, oldClaimConditions.length); + + ext.setClaimConditions(newClaimConditions, true); + + (uint256 newStartIndex, uint256 newCount) = ext.claimCondition(); + assertEq(newCount, newClaimConditions.length); + } + + function test_setClaimConditions_resetEligibility_conditionState() + public + whenCallerAuthorized + whenCorrectTimestamps + { + ext.setClaimConditions(newClaimConditions, true); + + (uint256 newStartIndex, uint256 newCount) = ext.claimCondition(); + + for (uint256 i = 0; i < newCount; i++) { + IClaimCondition.ClaimCondition memory _claimCondition = ext.getClaimConditionById(i + newStartIndex); + + assertEq(_claimCondition.startTimestamp, newClaimConditions[i].startTimestamp); + assertEq(_claimCondition.maxClaimableSupply, newClaimConditions[i].maxClaimableSupply); + assertEq(_claimCondition.supplyClaimed, 0); + } + } + + function test_setClaimConditions_resetEligibility_oldConditionsDeleted() + public + whenCallerAuthorized + whenCorrectTimestamps + { + (uint256 oldStartIndex, uint256 oldCount) = ext.claimCondition(); + assertEq(oldCount, oldClaimConditions.length); + + ext.setClaimConditions(newClaimConditions, true); + + for (uint256 i = 0; i < oldCount; i++) { + IClaimCondition.ClaimCondition memory _claimCondition = ext.getClaimConditionById(i + oldStartIndex); + + assertEq(_claimCondition.startTimestamp, 0); + assertEq(_claimCondition.maxClaimableSupply, 0); + assertEq(_claimCondition.supplyClaimed, 0); + assertEq(_claimCondition.quantityLimitPerWallet, 0); + assertEq(_claimCondition.merkleRoot, bytes32(0)); + assertEq(_claimCondition.pricePerToken, 0); + assertEq(_claimCondition.currency, address(0)); + assertEq(_claimCondition.metadata, ""); + } + } + + function test_setClaimConditions_resetEligibility_event() public whenCallerAuthorized whenCorrectTimestamps { + // TODO: fix/review event data check by setting last param true + vm.expectEmit(false, false, false, false); + emit ClaimConditionsUpdated(newClaimConditions, true); + ext.setClaimConditions(newClaimConditions, true); + } + + // ================== + // ======= Test branch: claim eligibility not reset + // ================== + + function test_setClaimConditions_noReset_maxClaimableLessThanClaimed() + public + whenCallerAuthorized + whenCorrectTimestamps + { + IClaimCondition.ClaimCondition memory _oldCondition = ext.getClaimConditionById(0); + + // set new maxClaimableSupply less than supplyClaimed of the old condition + newClaimConditions[0].maxClaimableSupply = _oldCondition.supplyClaimed - 1; + + vm.expectRevert("max supply claimed"); + ext.setClaimConditions(newClaimConditions, false); + } + + modifier whenMaxClaimableNotLessThanClaimed() { + _; + } + + function test_setClaimConditions_noReset_startIndex() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + (uint256 oldStartIndex, ) = ext.claimCondition(); + + ext.setClaimConditions(newClaimConditions, false); + + (uint256 newStartIndex, ) = ext.claimCondition(); + assertEq(newStartIndex, oldStartIndex); + } + + function test_setClaimConditions_noReset_conditionCount() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + (, uint256 oldCount) = ext.claimCondition(); + assertEq(oldCount, oldClaimConditions.length); + + ext.setClaimConditions(newClaimConditions, false); + + (uint256 newStartIndex, uint256 newCount) = ext.claimCondition(); + assertEq(newCount, newClaimConditions.length); + } + + function test_setClaimConditions_noReset_conditionState() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + (, uint256 oldCount) = ext.claimCondition(); + + // setting array size as this way to avoid out-of-bound error in the second loop + uint256 length = newClaimConditions.length > oldCount ? newClaimConditions.length : oldCount; + IClaimCondition.ClaimCondition[] memory _oldConditions = new IClaimCondition.ClaimCondition[](length); + + for (uint256 i = 0; i < oldCount; i++) { + _oldConditions[i] = ext.getClaimConditionById(i); + } + + ext.setClaimConditions(newClaimConditions, false); + + (uint256 newStartIndex, uint256 newCount) = ext.claimCondition(); + + for (uint256 i = 0; i < newCount; i++) { + IClaimCondition.ClaimCondition memory _claimCondition = ext.getClaimConditionById(i + newStartIndex); + + assertEq(_claimCondition.startTimestamp, newClaimConditions[i].startTimestamp); + assertEq(_claimCondition.maxClaimableSupply, newClaimConditions[i].maxClaimableSupply); + assertEq(_claimCondition.supplyClaimed, _oldConditions[i].supplyClaimed); + } + } + + function test_setClaimConditions_resetEligibility_oldConditionsDeletedOrReplaced() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + (uint256 oldStartIndex, uint256 oldCount) = ext.claimCondition(); + assertEq(oldCount, oldClaimConditions.length); + + ext.setClaimConditions(newClaimConditions, false); + (, uint256 newCount) = ext.claimCondition(); + + for (uint256 i = 0; i < oldCount; i++) { + IClaimCondition.ClaimCondition memory _claimCondition = ext.getClaimConditionById(i + oldStartIndex); + + if (i >= newCount) { + // case where deleted + + assertEq(_claimCondition.startTimestamp, 0); + assertEq(_claimCondition.maxClaimableSupply, 0); + assertEq(_claimCondition.supplyClaimed, 0); + assertEq(_claimCondition.quantityLimitPerWallet, 0); + assertEq(_claimCondition.merkleRoot, bytes32(0)); + assertEq(_claimCondition.pricePerToken, 0); + assertEq(_claimCondition.currency, address(0)); + assertEq(_claimCondition.metadata, ""); + } else { + // case where replaced + + // supply claimed should be same as old condition, hence not checked below + + assertEq(_claimCondition.startTimestamp, newClaimConditions[i].startTimestamp); + assertEq(_claimCondition.maxClaimableSupply, newClaimConditions[i].maxClaimableSupply); + assertEq(_claimCondition.quantityLimitPerWallet, newClaimConditions[i].quantityLimitPerWallet); + assertEq(_claimCondition.merkleRoot, newClaimConditions[i].merkleRoot); + assertEq(_claimCondition.pricePerToken, newClaimConditions[i].pricePerToken); + assertEq(_claimCondition.currency, newClaimConditions[i].currency); + assertEq(_claimCondition.metadata, newClaimConditions[i].metadata); + } + } + } + + function test_setClaimConditions_noReset_event() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + // TODO: fix/review event data check by setting last param true + vm.expectEmit(false, false, false, false); + emit ClaimConditionsUpdated(newClaimConditions, false); + ext.setClaimConditions(newClaimConditions, false); + } +} diff --git a/src/test/sdk/extension/drop/set-claim-conditions/setClaimConditions.tree b/src/test/sdk/extension/drop/set-claim-conditions/setClaimConditions.tree new file mode 100644 index 000000000..dbf6297d3 --- /dev/null +++ b/src/test/sdk/extension/drop/set-claim-conditions/setClaimConditions.tree @@ -0,0 +1,24 @@ +setClaimConditions(ClaimCondition[] calldata _conditions, bool _resetClaimEligibility) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when start timestamps of new conditions aren't in ascending order + │ └── it should revert ✅ + └── when start timestamps of new conditions are in ascending order + ├── when claim eligibility is reset + │ └── it should set new conditions start index as the count of old conditions ✅ + │ └── it should set claim condition count equal to the count of new conditions ✅ + │ └── it should correctly save all new conditions at right index ✅ + │ └── it should set supply claimed for each condition equal to 0 ✅ + │ └── it should delete all old conditions (i.e. all conditions with index less than new start index) ✅ + │ └── it should emit ClaimConditionsUpdated event ✅ + └── when claim eligibility is not reset + ├── when maxClaimableSupply of a new condition is less than supplyClaimed of the old condition (at that index) + │ └── it should revert ✅ + └── when maxClaimableSupply of a new condition is greater than or equal to supplyClaimed of the old condition (at that index) + └── it should set new conditions start index same as old start index ✅ + └── it should set claim condition count equal to the count of new conditions ✅ + └── it should correctly save all new conditions at right index ✅ + └── it should set supply claimed for each condition equal to what it was in old condition (at that index) ✅ + └── it should delete all old conditions with index exceeding new count, in case new count is less than previous count ✅ + └── it should emit ClaimConditionsUpdated event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/drop/verify-claim/verifyClaim.t.sol b/src/test/sdk/extension/drop/verify-claim/verifyClaim.t.sol new file mode 100644 index 000000000..3365d84ac --- /dev/null +++ b/src/test/sdk/extension/drop/verify-claim/verifyClaim.t.sol @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Drop, IDrop, IClaimConditionMultiPhase, IClaimCondition } from "contracts/extension/Drop.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyDrop is Drop { + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override {} + + function _transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) + internal + override + returns (uint256 startTokenId) + {} + + function _canSetClaimConditions() internal view override returns (bool) { + return true; + } + + /** + * note: the functions below are dummy functions for test purposes, + * to directly access and set/reset state without going through the actual functions and checks + */ + + function setCondition(ClaimCondition calldata condition, uint256 _conditionId) public { + claimCondition.conditions[_conditionId] = condition; + } + + function setSupplyClaimedByWallet( + uint256 _conditionId, + address _wallet, + uint256 _supplyClaimed + ) public { + claimCondition.supplyClaimedByWallet[_conditionId][_wallet] = _supplyClaimed; + } +} + +contract Drop_VerifyClaim is ExtensionUtilTest { + MyDrop internal ext; + + uint256 internal _conditionId; + address internal _claimer; + address internal _allowlistClaimer; + uint256 internal _quantity; + address internal _currency; + uint256 internal _pricePerToken; + IDrop.AllowlistProof internal _allowlistProof; + IDrop.AllowlistProof internal _allowlistProofEmpty; // will leave uninitialized + + IClaimCondition.ClaimCondition internal claimCondition; + IClaimCondition.ClaimCondition internal claimConditionWithAllowlist; + + function setUp() public override { + super.setUp(); + + ext = new MyDrop(); + + _claimer = getActor(1); + _allowlistClaimer = address(0x92Bb439374a091c7507bE100183d8D1Ed2c9dAD3); + + // claim condition without allowlist + claimCondition = IClaimCondition.ClaimCondition({ + startTimestamp: 1000, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }); + + // claim condition with allowlist -- set defaults for now + claimConditionWithAllowlist = claimCondition; + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 0, // default + type(uint256).max, // default + address(0) // default + ); + } + + function _setAllowlistAndProofs( + uint256 _quantity, + uint256 _price, + address _currency + ) internal returns (IDrop.AllowlistProof memory, bytes32) { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = Strings.toString(_quantity); + inputs[3] = Strings.toString(_price); + inputs[4] = Strings.toHexString(uint160(_currency)); + + 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[])); + + IDrop.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = _quantity; + alp.pricePerToken = _price; + alp.currency = address(_currency); + + return (alp, root); + } + + // ================== + // ======= Test branch: when no allowlist + // ================== + + function test_verifyClaim_noAllowlist_invalidCurrency() public { + ext.setCondition(claimCondition, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenValidCurrency_open() { + _currency = claimCondition.currency; + _; + } + + function test_verifyClaim_noAllowlist_invalidPrice() public whenValidCurrency_open { + ext.setCondition(claimCondition, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenValidPrice_open() { + _pricePerToken = claimCondition.pricePerToken; + _; + } + + function test_verifyClaim_noAllowlist_zeroQuantity() public whenValidCurrency_open whenValidPrice_open { + ext.setCondition(claimCondition, _conditionId); + + _quantity = 0; + vm.expectRevert(bytes("!Qty")); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenNonZeroQuantity() { + _quantity = claimCondition.quantityLimitPerWallet + 1234; + _; + } + + function test_verifyClaim_noAllowlist_nonZeroInvalidQuantity() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + { + ext.setCondition(claimCondition, _conditionId); + ext.setSupplyClaimedByWallet(_conditionId, _claimer, claimCondition.quantityLimitPerWallet); + + vm.expectRevert(bytes("!Qty")); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenValidQuantity_open() { + _quantity = 1; + _; + } + + function test_verifyClaim_noAllowlist_quantityMoreThanMaxClaimableSupply() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + whenValidQuantity_open + { + claimCondition.supplyClaimed = claimCondition.maxClaimableSupply; + ext.setCondition(claimCondition, _conditionId); + + vm.expectRevert("!MaxSupply"); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenQuantityWithinMaxLimit() { + _; + } + + function test_verifyClaim_noAllowlist_beforeStartTimestamp() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + whenValidQuantity_open + whenQuantityWithinMaxLimit + { + ext.setCondition(claimCondition, _conditionId); + + vm.expectRevert("cant claim yet"); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenValidTimestamp() { + vm.warp(claimCondition.startTimestamp); + _; + } + + function test_verifyClaim_noAllowlist() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + whenValidQuantity_open + whenQuantityWithinMaxLimit + whenValidTimestamp + { + ext.setCondition(claimCondition, _conditionId); + + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + // ================== + // ======= Test branch: allowlist but incorrect proof -- open limits should apply + // ================== + + function test_verifyClaim_incorrectProof_invalidCurrency() public { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + function test_verifyClaim_incorrectProof_invalidPrice() public whenValidCurrency_open { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + function test_verifyClaim_incorrectProof_zeroQuantity() public whenValidCurrency_open whenValidPrice_open { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + _quantity = 0; + vm.expectRevert(bytes("!Qty")); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + function test_verifyClaim_incorrectProof_nonZeroInvalidQuantity() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + ext.setSupplyClaimedByWallet(_conditionId, _claimer, claimCondition.quantityLimitPerWallet); + + vm.expectRevert(bytes("!Qty")); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + function test_verifyClaim_incorrectProof() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + whenValidQuantity_open + whenQuantityWithinMaxLimit + whenValidTimestamp + { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + // ================== + // ======= Test branch: allowlist with correct proof + // ================== + + function test_verifyClaim_allowlist_defaultPriceAndCurrency_invalidCurrencyParam() public { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_defaultPriceNonDefaultCurrenct_invalidCurrencyParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 0, // default + type(uint256).max, // default + address(weth) + ); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_nonDefaultPriceAndCurrency_invalidCurrencyParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 0, // default + 2, + address(weth) + ); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_defaultQuantity_invalidQuantityParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 0, // default + 2, + address(weth) + ); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + ext.setSupplyClaimedByWallet( + _conditionId, + _allowlistClaimer, + claimConditionWithAllowlist.quantityLimitPerWallet + ); + + _currency = address(weth); + _pricePerToken = 2; + _quantity = 1; + vm.expectRevert(bytes("!Qty")); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_nonDefaultQuantity_invalidQuantityParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs(5, 2, address(weth)); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + ext.setSupplyClaimedByWallet(_conditionId, _allowlistClaimer, 5); + + _currency = address(weth); + _pricePerToken = 2; + _quantity = 1; + vm.expectRevert(bytes("!Qty")); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_defaultPrice_invalidPriceParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 5, + type(uint256).max, // default + address(weth) + ); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + _currency = address(weth); + _quantity = 1; + vm.expectRevert(bytes("!PriceOrCurrency")); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_nonDefaultPrice_invalidPriceParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs(5, 1, address(weth)); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + _currency = address(weth); + _quantity = 1; + _pricePerToken = 2; + vm.expectRevert(bytes("!PriceOrCurrency")); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist() public whenQuantityWithinMaxLimit whenValidTimestamp { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs(5, 1, address(weth)); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + _currency = address(weth); + _quantity = 1; + _pricePerToken = 1; + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } +} diff --git a/src/test/sdk/extension/drop/verify-claim/verifyClaim.tree b/src/test/sdk/extension/drop/verify-claim/verifyClaim.tree new file mode 100644 index 000000000..64553ab90 --- /dev/null +++ b/src/test/sdk/extension/drop/verify-claim/verifyClaim.tree @@ -0,0 +1,67 @@ +verifyClaim( + uint256 conditionId, + address claimer, + uint256 quantity, + address currency, + uint256 pricePerToken, + AllowlistProof calldata allowlistProof +) +├── when no allowlist + └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when currency param is equal to open claim currency + └── when pricePerToken param not equal to open claim price + │ └── it should revert ✅ + └── when pricePerToken param is equal to open claim price + └── when quantity param is 0 + │ └── it should revert ✅ + └── when quantity param is not 0 + └── when quantity param plus supply claimed is more than open claim limit + │ └── it should revert ✅ + └── when quantity param plus supply claimed is within open claim limit + └── when quantity param plus claimed supply is more than max claimable supply + │ └── it should revert ✅ + └── when quantity param plus claimed supply is within max claimable supply limit + └── when block timestamp is less than start timestamp of claim phase + │ └── it should revert ✅ + └── when block timestamp is greater than or equal to start timestamp of claim phase + └── execution completes -- exit function ✅ + +├── when allowlist but incorrect merkle proof + └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when currency param is equal to open claim currency + └── when pricePerToken param not equal to open claim price + │ └── it should revert ✅ + └── when pricePerToken param is equal to open claim price + └── when quantity param is 0 + │ └── it should revert ✅ + └── when quantity param is not 0 + └── when quantity param plus supply claimed is more than open claim limit + │ └── it should revert ✅ + +├── when allowlist and correct merkle proof + └── when allowlist price is default max uint256 and allowlist currency is default address(0) + │ └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when allowlist price is default max uint256 and allowlist currency is not default + │ └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when allowlist price is not default and allowlist currency is default address(0) + │ └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when allowlist price is not default and allowlist currency is not default + │ └── when currency param not equal to allowlist claim currency + │ └── it should revert ✅ + └── when allowlist quantity is default 0 + │ └── when nonzero quantity param plus supply claimed is more than open claim limit + │ └── it should revert ✅ + └── when allowlist quantity is not default + │ └── when nonzero quantity param plus supply claimed is more than allowlist claim limit + │ └── it should revert ✅ + └── when allowlist price is default max uint256 + │ └── when pricePerToken param not equal to open claim price + │ └── it should revert ✅ + └── when allowlist price is not default + │ └── when pricePerToken param not equal to allowlist claim price + │ └── it should revert ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/lazy-mint/lazy-mint/lazyMint.t.sol b/src/test/sdk/extension/lazy-mint/lazy-mint/lazyMint.t.sol new file mode 100644 index 000000000..135980bad --- /dev/null +++ b/src/test/sdk/extension/lazy-mint/lazy-mint/lazyMint.t.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { LazyMint, BatchMintMetadata } from "contracts/extension/LazyMint.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyLazyMint is LazyMint { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _canLazyMint() internal view override returns (bool) { + return msg.sender == admin; + } + + function getBaseURI(uint256 _tokenId) external view returns (string memory) { + return _getBaseURI(_tokenId); + } + + function getBatchStartId(uint256 _batchID) public view returns (uint256) { + return _getBatchStartId(_batchID); + } + + function nextTokenIdToMint() public view returns (uint256) { + return nextTokenIdToLazyMint; + } +} + +contract LazyMint_LazyMint is ExtensionUtilTest { + MyLazyMint internal ext; + uint256 internal startId; + uint256 internal amount; + uint256[] internal batchIds; + address internal admin; + address internal caller; + + event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + + ext = new MyLazyMint(address(admin)); + + startId = 0; + // mint 5 batches + vm.startPrank(admin); + for (uint256 i = 0; i < 5; i++) { + uint256 _amount = (i + 1) * 10; + uint256 batchId = startId + _amount; + batchIds.push(batchId); + + string memory baseURI = Strings.toString(batchId); + startId = ext.lazyMint(_amount, baseURI, ""); + } + vm.stopPrank(); + } + + function test_lazyMint_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + ext.lazyMint(amount, "", ""); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_lazyMint_zeroAmount() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectRevert("0 amt"); + ext.lazyMint(amount, "", ""); + } + + modifier whenAmountNotZero() { + amount = 50; + _; + } + + function test_lazyMint() public whenCallerAuthorized whenAmountNotZero { + // check previous state + uint256 _nextTokenIdToLazyMintOld = ext.nextTokenIdToMint(); + assertEq(_nextTokenIdToLazyMintOld, batchIds[4]); + + string memory baseURI = "ipfs://baseURI"; + + // lazy mint next batch + vm.prank(address(caller)); + uint256 _batchId = ext.lazyMint(amount, baseURI, ""); + + // check new state + uint256 _batchStartId = ext.getBatchStartId(_batchId); + assertEq(_nextTokenIdToLazyMintOld, _batchStartId); + assertEq(_batchId, _nextTokenIdToLazyMintOld + amount); + for (uint256 i = _batchStartId; i < _batchId; i++) { + assertEq(ext.getBaseURI(i), baseURI); + } + assertEq(ext.nextTokenIdToMint(), _nextTokenIdToLazyMintOld + amount); + } + + function test_lazyMint_event() public whenCallerAuthorized whenAmountNotZero { + string memory baseURI = "ipfs://baseURI"; + uint256 _nextTokenIdToLazyMintOld = ext.nextTokenIdToMint(); + + // lazy mint next batch + vm.prank(address(caller)); + vm.expectEmit(); + emit TokensLazyMinted(_nextTokenIdToLazyMintOld, _nextTokenIdToLazyMintOld + amount - 1, baseURI, ""); + ext.lazyMint(amount, baseURI, ""); + } +} diff --git a/src/test/sdk/extension/lazy-mint/lazy-mint/lazyMint.tree b/src/test/sdk/extension/lazy-mint/lazy-mint/lazyMint.tree new file mode 100644 index 000000000..72ac4ddb3 --- /dev/null +++ b/src/test/sdk/extension/lazy-mint/lazy-mint/lazyMint.tree @@ -0,0 +1,17 @@ +lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + bytes calldata _data +) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when amount to lazy mint is 0 + │ └── it should revert ✅ + └── when amount to lazy mint is not 0 + └── it should save the batch of tokens starting at `nextTokenIdToLazyMint` ✅ + └── it should store batch id equal to the sum of `nextTokenIdToLazyMint` and `_amount` ✅ + └── it should map the new batch id to `_baseURIForTokens` ✅ + └── it should increase `nextTokenIdToLazyMint` by `_amount` ✅ + └── it should return the new `batchId` ✅ + └── it should emit TokensLazyMinted event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/ownable/set-owner/setOwner.t.sol b/src/test/sdk/extension/ownable/set-owner/setOwner.t.sol new file mode 100644 index 000000000..328c53d2a --- /dev/null +++ b/src/test/sdk/extension/ownable/set-owner/setOwner.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Ownable, IOwnable } from "contracts/extension/Ownable.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyOwnable is Ownable { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _canSetOwner() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract Ownable_SetOwner is ExtensionUtilTest { + MyOwnable internal ext; + address internal admin; + address internal caller; + address internal oldOwner; + address internal newOwner; + + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + + oldOwner = getActor(2); + newOwner = getActor(3); + + ext = new MyOwnable(address(admin)); + + vm.prank(address(admin)); + ext.setOwner(oldOwner); + + assertEq(oldOwner, ext.owner()); + } + + function test_setOwner_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + ext.setOwner(newOwner); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setOwner() public whenCallerAuthorized { + vm.prank(address(caller)); + ext.setOwner(newOwner); + + assertEq(newOwner, ext.owner()); + } + + function test_setOwner_event() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectEmit(true, true, false, false); + emit OwnerUpdated(oldOwner, newOwner); + ext.setOwner(newOwner); + } +} diff --git a/src/test/sdk/extension/ownable/set-owner/setOwner.tree b/src/test/sdk/extension/ownable/set-owner/setOwner.tree new file mode 100644 index 000000000..9db2c0a70 --- /dev/null +++ b/src/test/sdk/extension/ownable/set-owner/setOwner.tree @@ -0,0 +1,6 @@ +setContractURI(string memory uri) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── it should update owner by replacing old owner with the new owner input ✅ + └── it should emit OwnerUpdated event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol b/src/test/sdk/extension/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol new file mode 100644 index 000000000..a218d1125 --- /dev/null +++ b/src/test/sdk/extension/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Royalty, IRoyalty } from "contracts/extension/Royalty.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyRoyalty is Royalty { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {} + + function _canSetRoyaltyInfo() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract Royalty_SetDefaultRoyaltyInfo is ExtensionUtilTest { + MyRoyalty internal ext; + address internal admin; + address internal caller; + address internal defaultRoyaltyRecipient; + uint256 internal defaultRoyaltyBps; + + event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + defaultRoyaltyRecipient = getActor(2); + + ext = new MyRoyalty(address(admin)); + } + + function test_setDefaultRoyaltyInfo_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setDefaultRoyaltyInfo_exceedMaxBps() public whenCallerAuthorized { + defaultRoyaltyBps = 10_001; + vm.prank(address(caller)); + vm.expectRevert("Exceeds max bps"); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + modifier whenNotExceedMaxBps() { + defaultRoyaltyBps = 500; + _; + } + + function test_setDefaultRoyaltyInfo() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + + // get default royalty info + (address _recipient, uint16 _royaltyBps) = ext.getDefaultRoyaltyInfo(); + assertEq(_recipient, defaultRoyaltyRecipient); + assertEq(_royaltyBps, uint16(defaultRoyaltyBps)); + + // get royalty info for token + uint256 tokenId = 0; + (_recipient, _royaltyBps) = ext.getRoyaltyInfoForToken(tokenId); + assertEq(_recipient, defaultRoyaltyRecipient); + assertEq(_royaltyBps, uint16(defaultRoyaltyBps)); + + // royaltyInfo - ERC2981 + uint256 salePrice = 1000; + (address _royaltyRecipient, uint256 _royaltyAmount) = ext.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); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } +} diff --git a/src/test/sdk/extension/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.tree b/src/test/sdk/extension/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.tree new file mode 100644 index 000000000..78a4312de --- /dev/null +++ b/src/test/sdk/extension/royalty/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/sdk/extension/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol b/src/test/sdk/extension/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol new file mode 100644 index 000000000..e167be3db --- /dev/null +++ b/src/test/sdk/extension/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Royalty, IRoyalty } from "contracts/extension/Royalty.sol"; +import "../../ExtensionUtilTest.sol"; + +contract MyRoyalty is Royalty { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {} + + function _canSetRoyaltyInfo() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract Royalty_SetRoyaltyInfoForToken is ExtensionUtilTest { + MyRoyalty internal ext; + address internal admin; + address internal caller; + address internal defaultRoyaltyRecipient; + uint256 internal defaultRoyaltyBps; + + 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(); + + admin = getActor(0); + caller = getActor(1); + defaultRoyaltyRecipient = getActor(2); + royaltyRecipientForToken = getActor(3); + defaultRoyaltyBps = 500; + tokenId = 1; + + ext = new MyRoyalty(address(admin)); + + vm.prank(address(admin)); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + function test_setRoyaltyInfoForToken_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + ext.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setRoyaltyInfoForToken_exceedMaxBps() public whenCallerAuthorized { + royaltyBpsForToken = 10_001; + vm.prank(address(caller)); + vm.expectRevert("Exceeds max bps"); + ext.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } + + modifier whenNotExceedMaxBps() { + royaltyBpsForToken = 1000; + _; + } + + function test_setRoyaltyInfoForToken() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + ext.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + + // get default royalty info + (address _defaultRecipient, uint16 _defaultRoyaltyBps) = ext.getDefaultRoyaltyInfo(); + assertEq(_defaultRecipient, defaultRoyaltyRecipient); + assertEq(_defaultRoyaltyBps, uint16(defaultRoyaltyBps)); + + // get royalty info for token + (address _royaltyRecipientForToken, uint16 _royaltyBpsForToken) = ext.getRoyaltyInfoForToken(tokenId); + assertEq(_royaltyRecipientForToken, royaltyRecipientForToken); + assertEq(_royaltyBpsForToken, uint16(royaltyBpsForToken)); + + // royaltyInfo - ERC2981: calculate for default + uint256 salePrice = 1000; + (address _royaltyRecipient, uint256 _royaltyAmount) = ext.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) = ext.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); + ext.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } +} diff --git a/src/test/sdk/extension/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.tree b/src/test/sdk/extension/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.tree new file mode 100644 index 000000000..e28295634 --- /dev/null +++ b/src/test/sdk/extension/royalty/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 DefaultRoyalty event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.t.sol b/src/test/sdk/extension/upgradeable/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.t.sol new file mode 100644 index 000000000..6ca105a6f --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadataUpg is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function getBaseURI(uint256 _batchId) external view returns (string memory) { + return _batchMintMetadataStorage().baseURI[_batchId]; + } +} + +contract UpgradeableBatchMintMetadata_BatchMintMetadata is ExtensionUtilTest { + MyBatchMintMetadataUpg internal ext; + uint256 internal startId; + uint256 internal amountToMint; + string internal baseURI; + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadataUpg(); + startId = 20; + amountToMint = 100; + baseURI = "ipfs://baseURI"; + } + + function test_batchMintMetadata() public { + uint256 prevBaseURICount = ext.getBaseURICount(); + uint256 batchId = startId + amountToMint; + + ext.batchMintMetadata(startId, amountToMint, baseURI); + uint256 newBaseURICount = ext.getBaseURICount(); + assertEq(ext.getBaseURI(batchId), baseURI); + assertEq(newBaseURICount, prevBaseURICount + 1); + assertEq(ext.getBatchIdAtIndex(newBaseURICount - 1), batchId); + + vm.expectRevert("Invalid index"); + ext.getBatchIdAtIndex(newBaseURICount); + } +} diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.tree b/src/test/sdk/extension/upgradeable/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.tree new file mode 100644 index 000000000..572dd5203 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/batch-mint-metadata/_batchMintMetadata.tree @@ -0,0 +1,7 @@ +_batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens +) +├── it should store batch id equal to the sum of `_startId` and `_amountToMint` in batchIds array ✅ +├── it should map the new batch id to `_baseURIForTokens` ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.t.sol b/src/test/sdk/extension/upgradeable/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.t.sol new file mode 100644 index 000000000..9bc777bf4 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.t.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadataUpg is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function freezeBaseURI(uint256 _batchId) external { + _freezeBaseURI(_batchId); + } + + function batchFrozen(uint256 _batchId) external view returns (bool) { + return _batchMintMetadataStorage().batchFrozen[_batchId]; + } +} + +contract UpgradeableBatchMintMetadata_FreezeBaseURI is ExtensionUtilTest { + MyBatchMintMetadataUpg internal ext; + uint256 internal startId; + uint256[] internal batchIds; + uint256 internal indexToFreeze; + + event MetadataFrozen(); + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadataUpg(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + uint256 batchId = startId + amount; + batchIds.push(batchId); + + (startId, ) = ext.batchMintMetadata(startId, amount, "ipfs://"); + assertEq(ext.batchFrozen(batchId), false); + } + + indexToFreeze = 3; + } + + function test_freezeBaseURI_invalidBatch() public { + vm.expectRevert("Invalid batch"); + ext.freezeBaseURI(batchIds[indexToFreeze] * 10); // non-existent batchId + } + + modifier whenBatchIdValid() { + _; + } + + function test_freezeBaseURI() public whenBatchIdValid { + ext.freezeBaseURI(batchIds[indexToFreeze]); + + assertEq(ext.batchFrozen(batchIds[indexToFreeze]), true); + } + + function test_freezeBaseURI_event() public whenBatchIdValid { + vm.expectEmit(false, false, false, false); + emit MetadataFrozen(); + ext.freezeBaseURI(batchIds[indexToFreeze]); + } +} diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.tree b/src/test/sdk/extension/upgradeable/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.tree new file mode 100644 index 000000000..4dd87edef --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/freeze-base-uri/_freezeBaseURI.tree @@ -0,0 +1,6 @@ +_freezeBaseURI(uint256 _batchId) +├── when there is no baseURI for given `_batchId` + │ └── it should revert ✅ + └── when there is a baseURI present for given `_batchId` + └── it should freeze the `batchId` by setting `frozen[_batchId]` to `true` ✅ + └── it should emit MetadataFrozen event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-base-uri/_getBaseURI.t.sol b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-base-uri/_getBaseURI.t.sol new file mode 100644 index 000000000..153915408 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-base-uri/_getBaseURI.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadataUpg is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function getBaseURI(uint256 _tokenId) external view returns (string memory) { + return _getBaseURI(_tokenId); + } +} + +contract UpgradeableBatchMintMetadata_GetBaseURI is ExtensionUtilTest { + MyBatchMintMetadataUpg internal ext; + uint256 internal startId; + uint256[] internal batchIds; + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadataUpg(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + uint256 batchId = startId + amount; + batchIds.push(batchId); + + string memory baseURI = Strings.toString(batchId); + (startId, ) = ext.batchMintMetadata(startId, amount, baseURI); + } + } + + function test_getBaseURI_invalidTokenId() public { + uint256 tokenId = batchIds[4]; // tokenId greater than the last batchId + + vm.expectRevert("Invalid tokenId"); + ext.getBaseURI(tokenId); + } + + modifier whenValidTokenId() { + _; + } + + function test_getBaseURI() public whenValidTokenId { + for (uint256 i = 0; i < 5; i++) { + uint256 start = i == 0 ? 0 : batchIds[i - 1]; + for (uint256 j = start; j < batchIds[i]; j++) { + string memory _baseURI = ext.getBaseURI(j); + + assertEq(_baseURI, Strings.toString(batchIds[i])); + } + } + } +} diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-base-uri/_getBaseURI.tree b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-base-uri/_getBaseURI.tree new file mode 100644 index 000000000..c4ee674bf --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-base-uri/_getBaseURI.tree @@ -0,0 +1,6 @@ +_getBaseURI(uint256 _tokenId) +├── when `_tokenId` doesn't belong to any batch, i.e. greater than the last batchId + │ └── it should revert ✅ + └── when `_tokenId` belongs to some batch, i.e. less than that batchId + └── it should return correct baseURI for the `_tokenId` ✅ +(note: all batches are assumed to be contiguous, i.e. start id of one batch is the end id of the previous batch) \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-id/_getBatchId.t.sol b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-id/_getBatchId.t.sol new file mode 100644 index 000000000..9c8ec1136 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-id/_getBatchId.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadataUpg is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function getBatchId(uint256 _tokenId) external view returns (uint256 batchId, uint256 index) { + return _getBatchId(_tokenId); + } +} + +contract UpgradeableBatchMintMetadata_GetBatchId is ExtensionUtilTest { + MyBatchMintMetadataUpg internal ext; + uint256 internal startId; + uint256[] internal batchIds; + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadataUpg(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + batchIds.push(startId + amount); + (startId, ) = ext.batchMintMetadata(startId, amount, "ipfs://"); + } + } + + function test_getBatchId_invalidTokenId() public { + uint256 tokenId = batchIds[4]; // tokenId greater than the last batchId + + vm.expectRevert("Invalid tokenId"); + ext.getBatchId(tokenId); + } + + modifier whenValidTokenId() { + _; + } + + function test_getBatchId() public whenValidTokenId { + for (uint256 i = 0; i < 5; i++) { + uint256 start = i == 0 ? 0 : batchIds[i - 1]; + for (uint256 j = start; j < batchIds[i]; j++) { + (uint256 batchId, uint256 index) = ext.getBatchId(j); + + assertEq(batchId, batchIds[i]); + assertEq(index, i); + } + } + } +} diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-id/_getBatchId.tree b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-id/_getBatchId.tree new file mode 100644 index 000000000..2e6dd366e --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-id/_getBatchId.tree @@ -0,0 +1,6 @@ +_getBatchId(uint256 _tokenId) +├── when `_tokenId` doesn't belong to any batch, i.e. greater than the last batchId + │ └── it should revert ✅ + └── when `_tokenId` belongs to some batch, i.e. less than that batchId + └── it should return correct batchId and batch index for the `_tokenId` ✅ +(note: all batches are assumed to be contiguous, i.e. start id of one batch is the end id of the previous batch) \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-start-id/_getBatchStartId.t.sol b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-start-id/_getBatchStartId.t.sol new file mode 100644 index 000000000..6a6182a3b --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-start-id/_getBatchStartId.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadataUpg is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function getBatchStartId(uint256 _batchId) external view returns (uint256) { + return _getBatchStartId(_batchId); + } +} + +contract UpgradeableBatchMintMetadata_GetBatchStartId is ExtensionUtilTest { + MyBatchMintMetadataUpg internal ext; + uint256 internal startId; + uint256[] internal batchIds; + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadataUpg(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + batchIds.push(startId + amount); + (startId, ) = ext.batchMintMetadata(startId, amount, "ipfs://"); + } + } + + function test_getBatchStartId_invalidBatchId() public { + uint256 batchId = batchIds[4] + 1; // non-existent batchId + + vm.expectRevert("Invalid batchId"); + ext.getBatchStartId(batchId); + } + + modifier whenValidBatchId() { + _; + } + + function test_getBatchStartId() public whenValidBatchId { + for (uint256 i = 0; i < 5; i++) { + uint256 start = i == 0 ? 0 : batchIds[i - 1]; + uint256 _batchStartId = ext.getBatchStartId(batchIds[i]); + + assertEq(start, _batchStartId); + } + } +} diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-start-id/_getBatchStartId.tree b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-start-id/_getBatchStartId.tree new file mode 100644 index 000000000..7e303ab46 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/get-batch-start-id/_getBatchStartId.tree @@ -0,0 +1,6 @@ +_getBatchStartId(uint256 _batchID) +├── when `_batchID` doesn't exist + │ └── it should revert ✅ + └── when `_batchID` exists + └── it should return the starting tokenId for that batch ✅ +(note: all batches are assumed to be contiguous, i.e. start id of one batch is the end id of the previous batch) \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/set-base-uri/_setBaseURI.t.sol b/src/test/sdk/extension/upgradeable/batch-mint-metadata/set-base-uri/_setBaseURI.t.sol new file mode 100644 index 000000000..28a351d76 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/set-base-uri/_setBaseURI.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BatchMintMetadata } from "contracts/extension/upgradeable/BatchMintMetadata.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyBatchMintMetadataUpg is BatchMintMetadata { + function batchMintMetadata( + uint256 _startId, + uint256 _amountToMint, + string memory _baseURIForTokens + ) external returns (uint256 nextTokenIdToMint, uint256 batchId) { + (nextTokenIdToMint, batchId) = _batchMintMetadata(_startId, _amountToMint, _baseURIForTokens); + } + + function setBaseURI(uint256 _batchId, string memory _baseURI) external { + _setBaseURI(_batchId, _baseURI); + } + + function freezeBaseURI(uint256 _batchId, bool _freeze) external { + _batchMintMetadataStorage().batchFrozen[_batchId] = _freeze; + } + + function getBaseURI(uint256 _batchId) external view returns (string memory) { + return _batchMintMetadataStorage().baseURI[_batchId]; + } +} + +contract UpgradeableBatchMintMetadata_SetBaseURI is ExtensionUtilTest { + MyBatchMintMetadataUpg internal ext; + string internal newBaseURI; + uint256 internal startId; + uint256[] internal batchIds; + uint256 internal indexToUpdate; + + event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); + + function setUp() public override { + super.setUp(); + + ext = new MyBatchMintMetadataUpg(); + + startId = 0; + // mint 5 batches + for (uint256 i = 0; i < 5; i++) { + uint256 amount = (i + 1) * 10; + uint256 batchId = startId + amount; + batchIds.push(batchId); + + (startId, ) = ext.batchMintMetadata(startId, amount, "ipfs://"); + ext.freezeBaseURI(batchId, true); + } + + indexToUpdate = 3; + newBaseURI = "ipfs://baseURI"; + } + + function test_setBaseURI_frozenBatchId() public { + vm.expectRevert("Batch frozen"); + ext.setBaseURI(batchIds[indexToUpdate], newBaseURI); + } + + modifier whenBatchIdNotFrozen() { + ext.freezeBaseURI(batchIds[indexToUpdate], false); + _; + } + + function test_setBaseURI() public whenBatchIdNotFrozen { + ext.setBaseURI(batchIds[indexToUpdate], newBaseURI); + + string memory _baseURI = ext.getBaseURI(batchIds[indexToUpdate]); + + assertEq(_baseURI, newBaseURI); + } + + function test_setBaseURI_event() public whenBatchIdNotFrozen { + vm.expectEmit(false, false, false, true); + emit BatchMetadataUpdate(batchIds[indexToUpdate - 1], batchIds[indexToUpdate]); + ext.setBaseURI(batchIds[indexToUpdate], newBaseURI); + } +} diff --git a/src/test/sdk/extension/upgradeable/batch-mint-metadata/set-base-uri/_setBaseURI.tree b/src/test/sdk/extension/upgradeable/batch-mint-metadata/set-base-uri/_setBaseURI.tree new file mode 100644 index 000000000..3df76f653 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/batch-mint-metadata/set-base-uri/_setBaseURI.tree @@ -0,0 +1,6 @@ +_setBaseURI(uint256 _batchId, string memory _baseURI) +├── when the `_batchId` is frozen + │ └── it should revert ✅ + └── when the `_batchId` is not frozen + └── it should map the `_batchId` to `_baseURI` param ✅ + └── it should emit BatchMetadataUpdate event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.t.sol b/src/test/sdk/extension/upgradeable/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.t.sol new file mode 100644 index 000000000..a3aa6d57c --- /dev/null +++ b/src/test/sdk/extension/upgradeable/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.t.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BurnToClaim, IBurnToClaim } from "contracts/extension/upgradeable/BurnToClaim.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyBurnToClaimUpg is BurnToClaim { + function burnTokensOnOrigin( + address _tokenOwner, + uint256 _tokenId, + uint256 _quantity + ) public { + _burnTokensOnOrigin(_tokenOwner, _tokenId, _quantity); + } + + function _canSetBurnToClaim() internal view override returns (bool) { + return true; + } +} + +contract UpgradeableBurnToClaim_BurnTokensOnOrigin is ExtensionUtilTest { + MyBurnToClaimUpg internal ext; + Wallet internal tokenOwner; + uint256 internal tokenId; + uint256 internal quantity; + + function setUp() public override { + super.setUp(); + + ext = new MyBurnToClaimUpg(); + + tokenOwner = getWallet(); + erc721.mint(address(tokenOwner), 10); + erc1155.mint(address(tokenOwner), 1, 10); + + erc721NonBurnable.mint(address(tokenOwner), 10); + erc1155NonBurnable.mint(address(tokenOwner), 1, 10); + + tokenOwner.setApprovalForAllERC721(address(erc721), address(ext), true); + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(ext), true); + } + + // ================== + // ======= Test branch: token type is ERC721 + // ================== + + modifier whenNotBurnableERC721() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721NonBurnable), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + _; + } + + function test_burnTokensOnOrigin_ERC721_nonBurnable() public whenNotBurnableERC721 { + vm.expectRevert(); + ext.burnTokensOnOrigin(address(tokenOwner), tokenId, quantity); + } + + modifier whenBurnableERC721() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + _; + } + + function test_burnTokensOnOrigin_ERC721() public whenBurnableERC721 { + ext.burnTokensOnOrigin(address(tokenOwner), tokenId, quantity); + + assertEq(erc721.balanceOf(address(tokenOwner)), 9); + + vm.expectRevert(); + erc721.ownerOf(tokenId); // token doesn't exist after burning + } + + // ================== + // ======= Test branch: token type is ERC71155 + // ================== + + modifier whenNotBurnableERC1155() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155NonBurnable), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 1, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + _; + } + + function test_burnTokensOnOrigin_ERC1155_nonBurnable() public whenNotBurnableERC1155 { + vm.expectRevert(); + ext.burnTokensOnOrigin(address(tokenOwner), tokenId, quantity); + } + + modifier whenBurnableERC1155() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 1, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + _; + } + + function test_burnTokensOnOrigin_ERC1155() public whenBurnableERC1155 { + ext.burnTokensOnOrigin(address(tokenOwner), tokenId, quantity); + + assertEq(erc1155.balanceOf(address(tokenOwner), tokenId), 0); + } +} diff --git a/src/test/sdk/extension/upgradeable/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.tree b/src/test/sdk/extension/upgradeable/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.tree new file mode 100644 index 000000000..a2a3911ac --- /dev/null +++ b/src/test/sdk/extension/upgradeable/burn-to-claim/burn-tokens-on-origin/_burnTokensOnOrigin.tree @@ -0,0 +1,15 @@ +_burnTokensOnOrigin( + address _tokenOwner, + uint256 _tokenId, + uint256 _quantity +) +├── when burn-to-claim info has token type ERC721 + ├── when the origin ERC721 contract is not burnable + │ └── it should revert ✅ + └── when the origin ERC721 contract is burnable + └── it should successfully burn the token with given tokenId for the token owner ✅ +├── when burn-to-claim info has token type ERC1155 + ├── when the origin ERC1155 contract is not burnable + │ └── it should revert ✅ + └── when the origin ERC1155 contract is burnable + └── it should successfully burn tokens with given tokenId and quantity for the token owner ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.t.sol b/src/test/sdk/extension/upgradeable/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.t.sol new file mode 100644 index 000000000..cb3cf0133 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BurnToClaim, IBurnToClaim } from "contracts/extension/upgradeable/BurnToClaim.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyBurnToClaimUpg is BurnToClaim { + bool condition; + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _canSetBurnToClaim() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract UpgradeableBurnToClaim_SetBurnToClaimInfo is ExtensionUtilTest { + MyBurnToClaimUpg internal ext; + address internal admin; + address internal caller; + IBurnToClaim.BurnToClaimInfo internal info; + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + + ext = new MyBurnToClaimUpg(address(admin)); + info = IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(0), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(0) + }); + } + + function test_setBurnToClaimInfo_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized."); + ext.setBurnToClaimInfo(info); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setBurnToClaimInfo_invalidOriginContract_addressZero() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectRevert("Origin contract not set."); + ext.setBurnToClaimInfo(info); + } + + modifier whenValidOriginContract() { + info.originContractAddress = address(erc721); + _; + } + + function test_setBurnToClaimInfo_invalidCurrency_addressZero() public whenCallerAuthorized whenValidOriginContract { + vm.prank(address(caller)); + vm.expectRevert("Currency not set."); + ext.setBurnToClaimInfo(info); + } + + modifier whenValidCurrency() { + info.currency = address(erc20); + _; + } + + function test_setBurnToClaimInfo() public whenCallerAuthorized whenValidOriginContract whenValidCurrency { + vm.prank(address(caller)); + ext.setBurnToClaimInfo(info); + + IBurnToClaim.BurnToClaimInfo memory _info = ext.getBurnToClaimInfo(); + + assertEq(_info.originContractAddress, info.originContractAddress); + assertEq(_info.currency, info.currency); + } +} diff --git a/src/test/sdk/extension/upgradeable/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.tree b/src/test/sdk/extension/upgradeable/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.tree new file mode 100644 index 000000000..d6e347f5e --- /dev/null +++ b/src/test/sdk/extension/upgradeable/burn-to-claim/set-burn-to-claim-info/setBurnToClaimInfo.tree @@ -0,0 +1,11 @@ +setBurnToClaimInfo(BurnToClaimInfo calldata _burnToClaimInfo) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when input originContractAddress is address(0) + │ └── it should revert ✅ + └── when input originContractAddress is not address(0) + ├── when input currency is address(0) + │ └── it should revert ✅ + └── when input currency is not address(0) + └── it should save incoming struct values into burnToClaimInfo state ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.t.sol b/src/test/sdk/extension/upgradeable/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.t.sol new file mode 100644 index 000000000..030ea0bb2 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.t.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { BurnToClaim, IBurnToClaim } from "contracts/extension/upgradeable/BurnToClaim.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyBurnToClaimUpg is BurnToClaim { + bool condition; + + function setCondition(bool _condition) external { + condition = _condition; + } + + function _canSetBurnToClaim() internal view override returns (bool) { + return condition; + } +} + +contract UpgradeableBurnToClaim_VerifyBurnToClaim is ExtensionUtilTest { + MyBurnToClaimUpg internal ext; + address internal tokenOwner; + uint256 internal tokenId; + uint256 internal quantity; + + function setUp() public override { + super.setUp(); + + ext = new MyBurnToClaimUpg(); + ext.setCondition(true); + + tokenOwner = getActor(1); + erc721.mint(address(tokenOwner), 10); + erc1155.mint(address(tokenOwner), 1, 10); + } + + function test_verifyBurnToClaim_infoNotSet() public { + vm.expectRevert(); + ext.verifyBurnToClaim(tokenOwner, tokenId, 1); + } + + // ================== + // ======= Test branch: token type is ERC721 + // ================== + + modifier whenBurnToClaimInfoSetERC721() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc721), + tokenType: IBurnToClaim.TokenType.ERC721, + tokenId: 0, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + IBurnToClaim.BurnToClaimInfo memory info = ext.getBurnToClaimInfo(); + _; + } + + function test_verifyBurnToClaim_ERC721_quantity_not_1() public whenBurnToClaimInfoSetERC721 { + quantity = 10; + vm.expectRevert("Invalid amount"); + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); + } + + modifier whenQuantityParamisOne() { + quantity = 1; + _; + } + + function test_verifyBurnToClaim_ERC721_notOwnerOfToken() + public + whenBurnToClaimInfoSetERC721 + whenQuantityParamisOne + { + vm.expectRevert("!Owner"); + ext.verifyBurnToClaim(address(0x123), tokenId, quantity); // random address as owner + } + + modifier whenCorrectOwner() { + _; + } + + function test_verifyBurnToClaim_ERC721() + public + whenBurnToClaimInfoSetERC721 + whenQuantityParamisOne + whenCorrectOwner + { + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); + } + + // ================== + // ======= Test branch: token type is ERC1155 + // ================== + + modifier whenBurnToClaimInfoSetERC1155() { + ext.setBurnToClaimInfo( + IBurnToClaim.BurnToClaimInfo({ + originContractAddress: address(erc1155), + tokenType: IBurnToClaim.TokenType.ERC1155, + tokenId: 1, + mintPriceForNewToken: 0, + currency: address(erc20) + }) + ); + IBurnToClaim.BurnToClaimInfo memory info = ext.getBurnToClaimInfo(); + _; + } + + function test_verifyBurnToClaim_ERC1155_invalidTokenId() public whenBurnToClaimInfoSetERC1155 { + vm.expectRevert("Invalid token Id"); + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); // the tokenId here is 0, but eligible one is set as 1 above + } + + modifier whenCorrectTokenId() { + tokenId = 1; + _; + } + + function test_verifyBurnToClaim_ERC1155_balanceLessThanQuantity() + public + whenBurnToClaimInfoSetERC1155 + whenCorrectTokenId + { + quantity = 100; + vm.expectRevert("!Balance"); + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); // available balance is 10 + } + + modifier whenSufficientBalance() { + quantity = 10; + _; + } + + function test_verifyBurnToClaim_ERC1155() + public + whenBurnToClaimInfoSetERC1155 + whenCorrectTokenId + whenSufficientBalance + { + ext.verifyBurnToClaim(tokenOwner, tokenId, quantity); + } +} diff --git a/src/test/sdk/extension/upgradeable/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.tree b/src/test/sdk/extension/upgradeable/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.tree new file mode 100644 index 000000000..ffc4dba9d --- /dev/null +++ b/src/test/sdk/extension/upgradeable/burn-to-claim/verify-burn-to-claim/verifyBurnToClaim.tree @@ -0,0 +1,23 @@ +verifyBurnToClaim( + address tokenOwner, + uint256 tokenId, + uint256 quantity +) +├── when burn-to-claim info is not set + │ └── it should revert ✅ + └── when burn-to-claim info is set, with token type ERC721 + │ ├── when quantity param is not 1 + │ │ └── it should revert ✅ + │ └── when quantity param is 1 + │ ├── when token owner param is not the actual token owner + │ │ └── it should revert ✅ + │ └── when token owner param is the correct token owner + │ │ └── execution completes -- exit function ✅ + └── when burn-to-claim info is set, with token type ERC1155 + ├── when tokenId param doesn't match eligible tokenId + │ └── it should revert ✅ + └── when tokenId param matches eligible tokenId + ├── when token owner has balance less than quantity param + │ └── it should revert ✅ + └── when token owner has balance greater than or equal to quantity param + └── execution completes -- exit function ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/contract-metadata/set-contract-uri/setContractURI.t.sol b/src/test/sdk/extension/upgradeable/contract-metadata/set-contract-uri/setContractURI.t.sol new file mode 100644 index 000000000..0be6c9030 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/contract-metadata/set-contract-uri/setContractURI.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { ContractMetadata, IContractMetadata } from "contracts/extension/upgradeable/ContractMetadata.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyContractMetadataUpg is ContractMetadata { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _canSetContractURI() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract UpgradeableContractMetadata_SetContractURI is ExtensionUtilTest { + MyContractMetadataUpg internal ext; + address internal admin; + address internal caller; + string internal uri; + + event ContractURIUpdated(string prevURI, string newURI); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + uri = "ipfs://newUri"; + + ext = new MyContractMetadataUpg(address(admin)); + } + + function test_setContractURI_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + ext.setContractURI(uri); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setContractURI() public whenCallerAuthorized { + vm.prank(address(caller)); + ext.setContractURI(uri); + + string memory _updatedUri = ext.contractURI(); + assertEq(_updatedUri, uri); + } + + function test_setContractURI_event() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectEmit(false, false, false, true); + emit ContractURIUpdated("", uri); + ext.setContractURI(uri); + } +} diff --git a/src/test/sdk/extension/upgradeable/contract-metadata/set-contract-uri/setContractURI.tree b/src/test/sdk/extension/upgradeable/contract-metadata/set-contract-uri/setContractURI.tree new file mode 100644 index 000000000..e626d76e4 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/contract-metadata/set-contract-uri/setContractURI.tree @@ -0,0 +1,6 @@ +setContractURI(string memory uri) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── it should update contract URI to the new URI value ✅ + └── it should emit ContractURIUpdated event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/delayed-reveal/get-reveal-uri/getRevealURI.t.sol b/src/test/sdk/extension/upgradeable/delayed-reveal/get-reveal-uri/getRevealURI.t.sol new file mode 100644 index 000000000..62a10b844 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/delayed-reveal/get-reveal-uri/getRevealURI.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { DelayedReveal, IDelayedReveal } from "contracts/extension/upgradeable/DelayedReveal.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyDelayedRevealUpg is DelayedReveal { + function setEncryptedData(uint256 _batchId, bytes memory _encryptedData) external { + _setEncryptedData(_batchId, _encryptedData); + } + + function reveal(uint256 identifier, bytes calldata key) external returns (string memory revealedURI) {} +} + +contract UpgradeableDelayedReveal_GetRevealURI is ExtensionUtilTest { + MyDelayedRevealUpg internal ext; + string internal originalURI; + bytes internal encryptionKey; + bytes internal encryptedURI; + bytes internal encryptedData; + uint256 internal batchId; + bytes32 internal provenanceHash; + + function setUp() public override { + super.setUp(); + + ext = new MyDelayedRevealUpg(); + originalURI = "ipfs://original"; + encryptionKey = "key123"; + batchId = 1; + + provenanceHash = keccak256(abi.encodePacked(originalURI, encryptionKey, block.chainid)); + encryptedURI = ext.encryptDecrypt(bytes(originalURI), encryptionKey); + encryptedData = abi.encode(encryptedURI, provenanceHash); + } + + function test_getRevealURI_encryptedDataNotSet() public { + vm.expectRevert("Nothing to reveal"); + ext.getRevealURI(batchId, encryptionKey); + } + + modifier whenEncryptedDataIsSet() { + ext.setEncryptedData(batchId, encryptedData); + _; + } + + function test_getRevealURI_incorrectKey() public whenEncryptedDataIsSet { + bytes memory incorrectKey = "incorrect key"; + + vm.expectRevert("Incorrect key"); + ext.getRevealURI(batchId, incorrectKey); + } + + modifier whenCorrectKey() { + _; + } + + function test_getRevealURI() public whenEncryptedDataIsSet whenCorrectKey { + string memory revealedURI = ext.getRevealURI(batchId, encryptionKey); + + assertEq(originalURI, revealedURI); + } +} diff --git a/src/test/sdk/extension/upgradeable/delayed-reveal/get-reveal-uri/getRevealURI.tree b/src/test/sdk/extension/upgradeable/delayed-reveal/get-reveal-uri/getRevealURI.tree new file mode 100644 index 000000000..acb580468 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/delayed-reveal/get-reveal-uri/getRevealURI.tree @@ -0,0 +1,8 @@ +getRevealURI(uint256 _batchId, bytes calldata _key) +├── when there is no encrypted data set for the given batch id + │ └── it should revert ✅ + └── when there is an associated encrypted data present for the given batch id + ├── when the encryption key provided is incorrect + │ └── it should revert ✅ + └── when the encryption key provided is correct + └── it should correctly decrypt and return the original URI ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/delayed-reveal/set-encrypted-data/_setEncryptedData.t.sol b/src/test/sdk/extension/upgradeable/delayed-reveal/set-encrypted-data/_setEncryptedData.t.sol new file mode 100644 index 000000000..a499bad5e --- /dev/null +++ b/src/test/sdk/extension/upgradeable/delayed-reveal/set-encrypted-data/_setEncryptedData.t.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { DelayedReveal, IDelayedReveal } from "contracts/extension/upgradeable/DelayedReveal.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyDelayedRevealUpg is DelayedReveal { + function setEncryptedData(uint256 _batchId, bytes memory _encryptedData) external { + _setEncryptedData(_batchId, _encryptedData); + } + + function reveal(uint256 identifier, bytes calldata key) external returns (string memory revealedURI) {} +} + +contract UpgradeableDelayedReveal_SetEncryptedData is ExtensionUtilTest { + MyDelayedRevealUpg internal ext; + uint256 internal batchId; + bytes internal data; + + function setUp() public override { + super.setUp(); + + ext = new MyDelayedRevealUpg(); + batchId = 1; + data = "test"; + } + + function test_setEncryptedData() public { + ext.setEncryptedData(batchId, data); + + assertEq(true, ext.isEncryptedBatch(batchId)); + assertEq(ext.encryptedData(batchId), data); + } +} diff --git a/src/test/sdk/extension/upgradeable/delayed-reveal/set-encrypted-data/_setEncryptedData.tree b/src/test/sdk/extension/upgradeable/delayed-reveal/set-encrypted-data/_setEncryptedData.tree new file mode 100644 index 000000000..68f99a2c8 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/delayed-reveal/set-encrypted-data/_setEncryptedData.tree @@ -0,0 +1,3 @@ +_setEncryptedData(uint256 _batchId, bytes memory _encryptedData) +├── it should store input bytes data for the given batch id param ✅ +├── isEncryptedBatch should return true for this batch id ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/drop/claim/claim.t.sol b/src/test/sdk/extension/upgradeable/drop/claim/claim.t.sol new file mode 100644 index 000000000..3424f40e0 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/drop/claim/claim.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Drop, IDrop, IClaimConditionMultiPhase, IClaimCondition } from "contracts/extension/upgradeable/Drop.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyDropUpg is Drop { + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override {} + + function _transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) + internal + override + returns (uint256 startTokenId) + {} + + function _canSetClaimConditions() internal view override returns (bool) { + return true; + } + + function verifyClaim( + uint256 _conditionId, + address _claimer, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof + ) public view override returns (bool isOverride) {} +} + +contract UpgradeableDrop_Claim is ExtensionUtilTest { + MyDropUpg internal ext; + + address internal _claimer; + uint256 internal _quantity; + address internal _currency; + uint256 internal _pricePerToken; + IDrop.AllowlistProof internal _allowlistProof; + + IClaimCondition.ClaimCondition[] internal claimConditions; + + function setUp() public override { + super.setUp(); + + ext = new MyDropUpg(); + _claimer = getActor(1); + _quantity = 10; + } + + function _setConditionsState() public { + // values here are not important (except timestamp), since we won't be verifying claim params + + claimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 0, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + ext.setClaimConditions(claimConditions, false); + } + + function test_claim_noConditionsSet() public { + vm.expectRevert("!CONDITION."); + ext.claim(_claimer, _quantity, _currency, _pricePerToken, _allowlistProof, ""); + } + + modifier whenConditionsAreSet() { + _setConditionsState(); + _; + } + + function test_claim() public whenConditionsAreSet { + // claim + vm.prank(_claimer); + ext.claim(_claimer, _quantity, _currency, _pricePerToken, _allowlistProof, ""); + + uint256 supplyClaimedByWallet_1 = ext.getSupplyClaimedByWallet(0, _claimer); + uint256 supplyClaimed_1 = (ext.getClaimConditionById(0)).supplyClaimed; + + // claim again + vm.prank(_claimer); + ext.claim(_claimer, _quantity, _currency, _pricePerToken, _allowlistProof, ""); + + uint256 supplyClaimedByWallet_2 = ext.getSupplyClaimedByWallet(0, _claimer); + uint256 supplyClaimed_2 = (ext.getClaimConditionById(0)).supplyClaimed; + + // check state + assertEq(supplyClaimedByWallet_1, _quantity); + assertEq(supplyClaimedByWallet_2, supplyClaimedByWallet_1 + _quantity); + + assertEq(supplyClaimed_1, _quantity); + assertEq(supplyClaimed_2, supplyClaimed_1 + _quantity); + } +} diff --git a/src/test/sdk/extension/upgradeable/drop/claim/claim.tree b/src/test/sdk/extension/upgradeable/drop/claim/claim.tree new file mode 100644 index 000000000..4ca1d3187 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/drop/claim/claim.tree @@ -0,0 +1,15 @@ +claim( + address _receiver, + uint256 _quantity, + address _currency, + uint256 _pricePerToken, + AllowlistProof calldata _allowlistProof, + bytes memory _data +) +├── when no active condition + │ └── it should revert ✅ + └── when there's an active condition + └── it should increase the supplyClaimed for that condition by quantity param input ✅ + └── it should increase the supplyClaimedByWallet for that condition and msg.sender by quantity param input ✅ + +(Note: verifyClaim function has been tested separately, and hence not being tested here) \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/drop/get-active-claim-condition-id/getActiveClaimConditionId.t.sol b/src/test/sdk/extension/upgradeable/drop/get-active-claim-condition-id/getActiveClaimConditionId.t.sol new file mode 100644 index 000000000..5354cef86 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/drop/get-active-claim-condition-id/getActiveClaimConditionId.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Drop, IDrop, IClaimConditionMultiPhase, IClaimCondition } from "contracts/extension/upgradeable/Drop.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyDropUpg is Drop { + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override {} + + function _transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) + internal + override + returns (uint256 startTokenId) + {} + + function _canSetClaimConditions() internal view override returns (bool) { + return true; + } +} + +contract UpgradeableDrop_GetActiveClaimConditionId is ExtensionUtilTest { + MyDropUpg internal ext; + + IClaimCondition.ClaimCondition[] internal claimConditions; + + function setUp() public override { + super.setUp(); + + ext = new MyDropUpg(); + } + + function _setConditionsState() public { + claimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 100, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + claimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 200, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + claimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 300, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + ext.setClaimConditions(claimConditions, false); + } + + function test_getActiveClaimConditionId_noConditionsSet() public { + vm.expectRevert("!CONDITION."); + ext.getActiveClaimConditionId(); + } + + modifier whenConditionsAreSet() { + _setConditionsState(); + _; + } + + function test_getActiveClaimConditionId_noActiveCondition() public whenConditionsAreSet { + vm.expectRevert("!CONDITION."); + ext.getActiveClaimConditionId(); + } + + modifier whenActiveConditions() { + _; + } + + function test_getActiveClaimConditionId_activeConditions() public whenConditionsAreSet whenActiveConditions { + vm.warp(claimConditions[0].startTimestamp); + + uint256 id = ext.getActiveClaimConditionId(); + assertEq(id, 0); + + vm.warp(claimConditions[1].startTimestamp); + + id = ext.getActiveClaimConditionId(); + assertEq(id, 1); + + vm.warp(claimConditions[2].startTimestamp); + + id = ext.getActiveClaimConditionId(); + assertEq(id, 2); + } +} diff --git a/src/test/sdk/extension/upgradeable/drop/get-active-claim-condition-id/getActiveClaimConditionId.tree b/src/test/sdk/extension/upgradeable/drop/get-active-claim-condition-id/getActiveClaimConditionId.tree new file mode 100644 index 000000000..8b8a94d99 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/drop/get-active-claim-condition-id/getActiveClaimConditionId.tree @@ -0,0 +1,8 @@ +getActiveClaimConditionId() +├── when no conditions are set + │ └── it should revert ✅ + └── when condition(s) are set + ├── when no active condition, i.e. start timestamps of all conditions greater than block timestamp + │ └── it should revert ✅ + └── when conditions active, i.e. start timestamps at least one condition is less than or equal to the block timestamp + └── it should return the latest active claim condition id (i.e. with highest start timestamp among those active) ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/drop/set-claim-conditions/setClaimConditions.t.sol b/src/test/sdk/extension/upgradeable/drop/set-claim-conditions/setClaimConditions.t.sol new file mode 100644 index 000000000..179057de2 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/drop/set-claim-conditions/setClaimConditions.t.sol @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Drop, IDrop, IClaimConditionMultiPhase, IClaimCondition } from "contracts/extension/upgradeable/Drop.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyDropUpg is Drop { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override {} + + function _transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) + internal + override + returns (uint256 startTokenId) + {} + + function _canSetClaimConditions() internal view override returns (bool) { + return msg.sender == admin; + } + + /** + * note: the functions below are dummy functions for test purposes, + * to directly access and set/reset state without going through the actual functions and checks + */ + + function setCondition(ClaimCondition calldata condition, uint256 _conditionId) public { + _dropStorage().claimCondition.conditions[_conditionId] = condition; + } + + function setSupplyClaimedForCondition(uint256 _conditionId, uint256 _supplyClaimed) public { + _dropStorage().claimCondition.conditions[_conditionId].supplyClaimed = _supplyClaimed; + } +} + +contract UpgradeableDrop_SetClaimConditions is ExtensionUtilTest { + MyDropUpg internal ext; + address internal admin; + + IClaimCondition.ClaimCondition[] internal newClaimConditions; + IClaimCondition.ClaimCondition[] internal oldClaimConditions; + + event ClaimConditionsUpdated(IClaimCondition.ClaimCondition[] claimConditions, bool resetEligibility); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + ext = new MyDropUpg(admin); + + _setOldConditionsState(); + + newClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 100, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + newClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 200, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + } + + function _setOldConditionsState() public { + oldClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 10, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + oldClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 20, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + oldClaimConditions.push( + IClaimCondition.ClaimCondition({ + startTimestamp: 30, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }) + ); + + vm.prank(admin); + ext.setClaimConditions(oldClaimConditions, false); + (, uint256 count) = ext.claimCondition(); + assertEq(count, oldClaimConditions.length); + + ext.setSupplyClaimedForCondition(0, 5); + ext.setSupplyClaimedForCondition(0, 20); + ext.setSupplyClaimedForCondition(0, 100); + } + + function test_setClaimConditions_notAuthorized() public { + vm.expectRevert("Not authorized"); + ext.setClaimConditions(newClaimConditions, false); + + vm.expectRevert("Not authorized"); + ext.setClaimConditions(newClaimConditions, true); + } + + modifier whenCallerAuthorized() { + vm.startPrank(admin); + _; + vm.stopPrank(); + } + + function test_setClaimConditions_incorrectStartTimestamps() public whenCallerAuthorized { + // reverse the order of timestamps + newClaimConditions[0].startTimestamp = newClaimConditions[1].startTimestamp + 100; + + vm.expectRevert(bytes("ST")); + ext.setClaimConditions(newClaimConditions, false); + + vm.expectRevert(bytes("ST")); + ext.setClaimConditions(newClaimConditions, true); + } + + modifier whenCorrectTimestamps() { + _; + } + + // ================== + // ======= Test branch: claim eligibility reset + // ================== + + function test_setClaimConditions_resetEligibility_startIndex() public whenCallerAuthorized whenCorrectTimestamps { + (, uint256 oldCount) = ext.claimCondition(); + + ext.setClaimConditions(newClaimConditions, true); + + (uint256 newStartIndex, ) = ext.claimCondition(); + assertEq(newStartIndex, oldCount); + } + + function test_setClaimConditions_resetEligibility_conditionCount() + public + whenCallerAuthorized + whenCorrectTimestamps + { + (, uint256 oldCount) = ext.claimCondition(); + assertEq(oldCount, oldClaimConditions.length); + + ext.setClaimConditions(newClaimConditions, true); + + (uint256 newStartIndex, uint256 newCount) = ext.claimCondition(); + assertEq(newCount, newClaimConditions.length); + } + + function test_setClaimConditions_resetEligibility_conditionState() + public + whenCallerAuthorized + whenCorrectTimestamps + { + ext.setClaimConditions(newClaimConditions, true); + + (uint256 newStartIndex, uint256 newCount) = ext.claimCondition(); + + for (uint256 i = 0; i < newCount; i++) { + IClaimCondition.ClaimCondition memory _claimCondition = ext.getClaimConditionById(i + newStartIndex); + + assertEq(_claimCondition.startTimestamp, newClaimConditions[i].startTimestamp); + assertEq(_claimCondition.maxClaimableSupply, newClaimConditions[i].maxClaimableSupply); + assertEq(_claimCondition.supplyClaimed, 0); + } + } + + function test_setClaimConditions_resetEligibility_oldConditionsDeleted() + public + whenCallerAuthorized + whenCorrectTimestamps + { + (uint256 oldStartIndex, uint256 oldCount) = ext.claimCondition(); + assertEq(oldCount, oldClaimConditions.length); + + ext.setClaimConditions(newClaimConditions, true); + + for (uint256 i = 0; i < oldCount; i++) { + IClaimCondition.ClaimCondition memory _claimCondition = ext.getClaimConditionById(i + oldStartIndex); + + assertEq(_claimCondition.startTimestamp, 0); + assertEq(_claimCondition.maxClaimableSupply, 0); + assertEq(_claimCondition.supplyClaimed, 0); + assertEq(_claimCondition.quantityLimitPerWallet, 0); + assertEq(_claimCondition.merkleRoot, bytes32(0)); + assertEq(_claimCondition.pricePerToken, 0); + assertEq(_claimCondition.currency, address(0)); + assertEq(_claimCondition.metadata, ""); + } + } + + function test_setClaimConditions_resetEligibility_event() public whenCallerAuthorized whenCorrectTimestamps { + // TODO: fix/review event data check by setting last param true + vm.expectEmit(false, false, false, false); + emit ClaimConditionsUpdated(newClaimConditions, true); + ext.setClaimConditions(newClaimConditions, true); + } + + // ================== + // ======= Test branch: claim eligibility not reset + // ================== + + function test_setClaimConditions_noReset_maxClaimableLessThanClaimed() + public + whenCallerAuthorized + whenCorrectTimestamps + { + IClaimCondition.ClaimCondition memory _oldCondition = ext.getClaimConditionById(0); + + // set new maxClaimableSupply less than supplyClaimed of the old condition + newClaimConditions[0].maxClaimableSupply = _oldCondition.supplyClaimed - 1; + + vm.expectRevert("max supply claimed"); + ext.setClaimConditions(newClaimConditions, false); + } + + modifier whenMaxClaimableNotLessThanClaimed() { + _; + } + + function test_setClaimConditions_noReset_startIndex() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + (uint256 oldStartIndex, ) = ext.claimCondition(); + + ext.setClaimConditions(newClaimConditions, false); + + (uint256 newStartIndex, ) = ext.claimCondition(); + assertEq(newStartIndex, oldStartIndex); + } + + function test_setClaimConditions_noReset_conditionCount() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + (, uint256 oldCount) = ext.claimCondition(); + assertEq(oldCount, oldClaimConditions.length); + + ext.setClaimConditions(newClaimConditions, false); + + (uint256 newStartIndex, uint256 newCount) = ext.claimCondition(); + assertEq(newCount, newClaimConditions.length); + } + + function test_setClaimConditions_noReset_conditionState() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + (, uint256 oldCount) = ext.claimCondition(); + + // setting array size as this way to avoid out-of-bound error in the second loop + uint256 length = newClaimConditions.length > oldCount ? newClaimConditions.length : oldCount; + IClaimCondition.ClaimCondition[] memory _oldConditions = new IClaimCondition.ClaimCondition[](length); + + for (uint256 i = 0; i < oldCount; i++) { + _oldConditions[i] = ext.getClaimConditionById(i); + } + + ext.setClaimConditions(newClaimConditions, false); + + (uint256 newStartIndex, uint256 newCount) = ext.claimCondition(); + + for (uint256 i = 0; i < newCount; i++) { + IClaimCondition.ClaimCondition memory _claimCondition = ext.getClaimConditionById(i + newStartIndex); + + assertEq(_claimCondition.startTimestamp, newClaimConditions[i].startTimestamp); + assertEq(_claimCondition.maxClaimableSupply, newClaimConditions[i].maxClaimableSupply); + assertEq(_claimCondition.supplyClaimed, _oldConditions[i].supplyClaimed); + } + } + + function test_setClaimConditions_resetEligibility_oldConditionsDeletedOrReplaced() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + (uint256 oldStartIndex, uint256 oldCount) = ext.claimCondition(); + assertEq(oldCount, oldClaimConditions.length); + + ext.setClaimConditions(newClaimConditions, false); + (, uint256 newCount) = ext.claimCondition(); + + for (uint256 i = 0; i < oldCount; i++) { + IClaimCondition.ClaimCondition memory _claimCondition = ext.getClaimConditionById(i + oldStartIndex); + + if (i >= newCount) { + // case where deleted + + assertEq(_claimCondition.startTimestamp, 0); + assertEq(_claimCondition.maxClaimableSupply, 0); + assertEq(_claimCondition.supplyClaimed, 0); + assertEq(_claimCondition.quantityLimitPerWallet, 0); + assertEq(_claimCondition.merkleRoot, bytes32(0)); + assertEq(_claimCondition.pricePerToken, 0); + assertEq(_claimCondition.currency, address(0)); + assertEq(_claimCondition.metadata, ""); + } else { + // case where replaced + + // supply claimed should be same as old condition, hence not checked below + + assertEq(_claimCondition.startTimestamp, newClaimConditions[i].startTimestamp); + assertEq(_claimCondition.maxClaimableSupply, newClaimConditions[i].maxClaimableSupply); + assertEq(_claimCondition.quantityLimitPerWallet, newClaimConditions[i].quantityLimitPerWallet); + assertEq(_claimCondition.merkleRoot, newClaimConditions[i].merkleRoot); + assertEq(_claimCondition.pricePerToken, newClaimConditions[i].pricePerToken); + assertEq(_claimCondition.currency, newClaimConditions[i].currency); + assertEq(_claimCondition.metadata, newClaimConditions[i].metadata); + } + } + } + + function test_setClaimConditions_noReset_event() + public + whenCallerAuthorized + whenCorrectTimestamps + whenMaxClaimableNotLessThanClaimed + { + // TODO: fix/review event data check by setting last param true + vm.expectEmit(false, false, false, false); + emit ClaimConditionsUpdated(newClaimConditions, false); + ext.setClaimConditions(newClaimConditions, false); + } +} diff --git a/src/test/sdk/extension/upgradeable/drop/set-claim-conditions/setClaimConditions.tree b/src/test/sdk/extension/upgradeable/drop/set-claim-conditions/setClaimConditions.tree new file mode 100644 index 000000000..aecd6b06f --- /dev/null +++ b/src/test/sdk/extension/upgradeable/drop/set-claim-conditions/setClaimConditions.tree @@ -0,0 +1,24 @@ +setClaimConditions(ClaimCondition[] calldata _conditions, bool _resetClaimEligibility) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when start timestamps of new conditions aren't in ascending order + │ └── it should revert ✅ + └── when start timestamps of new conditions are in ascending order + ├── when claim eligibility is reset + │ └── it should set new conditions start index as the count of old conditions ✅ + │ └── it should set claim condition count equal to the count of new conditions ✅ + │ └── it should correctly save all new conditions at right index ✅ + │ └── it should set supply claimed for each condition equal to 0 ✅ + │ └── it should delete all old conditions (i.e. all conditions with index less than new start index) ✅ + │ └── it should emit ClaimConditionsUpdated event ✅ + └── when claim eligibility is not reset + ├── when maxClaimableSupply of a new condition is less than supplyClaimed of the old condition (at that index) + │ └── it should revert ✅ + └── when maxClaimableSupply of a new condition is greater than or equal to supplyClaimed of the old condition (at that index) + └── it should set new conditions start index same as old start index ✅ + └── it should set claim condition count equal to the count of new conditions ✅ + └── it should correctly save all new conditions at right index ✅ + └── it should set supply claimed for each condition equal to what it was in old condition (at that index) ✅ + └── it should delete all old conditions with index exceeding new count, in case new count is less than previous count ✅ + └── it should emit ClaimConditionsUpdated event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/drop/verify-claim/verifyClaim.t.sol b/src/test/sdk/extension/upgradeable/drop/verify-claim/verifyClaim.t.sol new file mode 100644 index 000000000..d71395817 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/drop/verify-claim/verifyClaim.t.sol @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Drop, IDrop, IClaimConditionMultiPhase, IClaimCondition } from "contracts/extension/upgradeable/Drop.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyDropUpg is Drop { + function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) internal override {} + + function _transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) + internal + override + returns (uint256 startTokenId) + {} + + function _canSetClaimConditions() internal view override returns (bool) { + return true; + } + + /** + * note: the functions below are dummy functions for test purposes, + * to directly access and set/reset state without going through the actual functions and checks + */ + + function setCondition(ClaimCondition calldata condition, uint256 _conditionId) public { + _dropStorage().claimCondition.conditions[_conditionId] = condition; + } + + function setSupplyClaimedByWallet( + uint256 _conditionId, + address _wallet, + uint256 _supplyClaimed + ) public { + _dropStorage().claimCondition.supplyClaimedByWallet[_conditionId][_wallet] = _supplyClaimed; + } +} + +contract UpgradeableDrop_VerifyClaim is ExtensionUtilTest { + MyDropUpg internal ext; + + uint256 internal _conditionId; + address internal _claimer; + address internal _allowlistClaimer; + uint256 internal _quantity; + address internal _currency; + uint256 internal _pricePerToken; + IDrop.AllowlistProof internal _allowlistProof; + IDrop.AllowlistProof internal _allowlistProofEmpty; // will leave uninitialized + + IClaimCondition.ClaimCondition internal claimCondition; + IClaimCondition.ClaimCondition internal claimConditionWithAllowlist; + + function setUp() public override { + super.setUp(); + + ext = new MyDropUpg(); + + _claimer = getActor(1); + _allowlistClaimer = address(0x92Bb439374a091c7507bE100183d8D1Ed2c9dAD3); + + // claim condition without allowlist + claimCondition = IClaimCondition.ClaimCondition({ + startTimestamp: 1000, + maxClaimableSupply: 100, + supplyClaimed: 0, + quantityLimitPerWallet: 1, + merkleRoot: bytes32(0), + pricePerToken: 10, + currency: address(erc20), + metadata: "" + }); + + // claim condition with allowlist -- set defaults for now + claimConditionWithAllowlist = claimCondition; + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 0, // default + type(uint256).max, // default + address(0) // default + ); + } + + function _setAllowlistAndProofs( + uint256 _quantity, + uint256 _price, + address _currency + ) internal returns (IDrop.AllowlistProof memory, bytes32) { + string[] memory inputs = new string[](5); + + inputs[0] = "node"; + inputs[1] = "src/test/scripts/generateRoot.ts"; + inputs[2] = Strings.toString(_quantity); + inputs[3] = Strings.toString(_price); + inputs[4] = Strings.toHexString(uint160(_currency)); + + 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[])); + + IDrop.AllowlistProof memory alp; + alp.proof = proofs; + alp.quantityLimitPerWallet = _quantity; + alp.pricePerToken = _price; + alp.currency = address(_currency); + + return (alp, root); + } + + // ================== + // ======= Test branch: when no allowlist + // ================== + + function test_verifyClaim_noAllowlist_invalidCurrency() public { + ext.setCondition(claimCondition, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenValidCurrency_open() { + _currency = claimCondition.currency; + _; + } + + function test_verifyClaim_noAllowlist_invalidPrice() public whenValidCurrency_open { + ext.setCondition(claimCondition, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenValidPrice_open() { + _pricePerToken = claimCondition.pricePerToken; + _; + } + + function test_verifyClaim_noAllowlist_zeroQuantity() public whenValidCurrency_open whenValidPrice_open { + ext.setCondition(claimCondition, _conditionId); + + _quantity = 0; + vm.expectRevert(bytes("!Qty")); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenNonZeroQuantity() { + _quantity = claimCondition.quantityLimitPerWallet + 1234; + _; + } + + function test_verifyClaim_noAllowlist_nonZeroInvalidQuantity() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + { + ext.setCondition(claimCondition, _conditionId); + ext.setSupplyClaimedByWallet(_conditionId, _claimer, claimCondition.quantityLimitPerWallet); + + vm.expectRevert(bytes("!Qty")); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenValidQuantity_open() { + _quantity = 1; + _; + } + + function test_verifyClaim_noAllowlist_quantityMoreThanMaxClaimableSupply() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + whenValidQuantity_open + { + claimCondition.supplyClaimed = claimCondition.maxClaimableSupply; + ext.setCondition(claimCondition, _conditionId); + + vm.expectRevert("!MaxSupply"); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenQuantityWithinMaxLimit() { + _; + } + + function test_verifyClaim_noAllowlist_beforeStartTimestamp() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + whenValidQuantity_open + whenQuantityWithinMaxLimit + { + ext.setCondition(claimCondition, _conditionId); + + vm.expectRevert("cant claim yet"); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + modifier whenValidTimestamp() { + vm.warp(claimCondition.startTimestamp); + _; + } + + function test_verifyClaim_noAllowlist() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + whenValidQuantity_open + whenQuantityWithinMaxLimit + whenValidTimestamp + { + ext.setCondition(claimCondition, _conditionId); + + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + // ================== + // ======= Test branch: allowlist but incorrect proof -- open limits should apply + // ================== + + function test_verifyClaim_incorrectProof_invalidCurrency() public { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + function test_verifyClaim_incorrectProof_invalidPrice() public whenValidCurrency_open { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + function test_verifyClaim_incorrectProof_zeroQuantity() public whenValidCurrency_open whenValidPrice_open { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + _quantity = 0; + vm.expectRevert(bytes("!Qty")); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + function test_verifyClaim_incorrectProof_nonZeroInvalidQuantity() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + ext.setSupplyClaimedByWallet(_conditionId, _claimer, claimCondition.quantityLimitPerWallet); + + vm.expectRevert(bytes("!Qty")); + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + function test_verifyClaim_incorrectProof() + public + whenValidCurrency_open + whenValidPrice_open + whenNonZeroQuantity + whenValidQuantity_open + whenQuantityWithinMaxLimit + whenValidTimestamp + { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + ext.verifyClaim(_conditionId, _claimer, _quantity, _currency, _pricePerToken, _allowlistProofEmpty); + } + + // ================== + // ======= Test branch: allowlist with correct proof + // ================== + + function test_verifyClaim_allowlist_defaultPriceAndCurrency_invalidCurrencyParam() public { + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_defaultPriceNonDefaultCurrenct_invalidCurrencyParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 0, // default + type(uint256).max, // default + address(weth) + ); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_nonDefaultPriceAndCurrency_invalidCurrencyParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 0, // default + 2, + address(weth) + ); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + vm.expectRevert("!PriceOrCurrency"); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_defaultQuantity_invalidQuantityParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 0, // default + 2, + address(weth) + ); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + ext.setSupplyClaimedByWallet( + _conditionId, + _allowlistClaimer, + claimConditionWithAllowlist.quantityLimitPerWallet + ); + + _currency = address(weth); + _pricePerToken = 2; + _quantity = 1; + vm.expectRevert(bytes("!Qty")); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_nonDefaultQuantity_invalidQuantityParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs(5, 2, address(weth)); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + ext.setSupplyClaimedByWallet(_conditionId, _allowlistClaimer, 5); + + _currency = address(weth); + _pricePerToken = 2; + _quantity = 1; + vm.expectRevert(bytes("!Qty")); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_defaultPrice_invalidPriceParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs( + 5, + type(uint256).max, // default + address(weth) + ); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + _currency = address(weth); + _quantity = 1; + vm.expectRevert(bytes("!PriceOrCurrency")); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist_nonDefaultPrice_invalidPriceParam() public { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs(5, 1, address(weth)); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + _currency = address(weth); + _quantity = 1; + _pricePerToken = 2; + vm.expectRevert(bytes("!PriceOrCurrency")); + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } + + function test_verifyClaim_allowlist() public whenQuantityWithinMaxLimit whenValidTimestamp { + (_allowlistProof, claimConditionWithAllowlist.merkleRoot) = _setAllowlistAndProofs(5, 1, address(weth)); + ext.setCondition(claimConditionWithAllowlist, _conditionId); + + _currency = address(weth); + _quantity = 1; + _pricePerToken = 1; + ext.verifyClaim(_conditionId, _allowlistClaimer, _quantity, _currency, _pricePerToken, _allowlistProof); + } +} diff --git a/src/test/sdk/extension/upgradeable/drop/verify-claim/verifyClaim.tree b/src/test/sdk/extension/upgradeable/drop/verify-claim/verifyClaim.tree new file mode 100644 index 000000000..ef84f4ef2 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/drop/verify-claim/verifyClaim.tree @@ -0,0 +1,67 @@ +verifyClaim( + uint256 conditionId, + address claimer, + uint256 quantity, + address currency, + uint256 pricePerToken, + AllowlistProof calldata allowlistProof +) +├── when no allowlist + └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when currency param is equal to open claim currency + └── when pricePerToken param not equal to open claim price + │ └── it should revert ✅ + └── when pricePerToken param is equal to open claim price + └── when quantity param is 0 + │ └── it should revert ✅ + └── when quantity param is not 0 + └── when quantity param plus supply claimed is more than open claim limit + │ └── it should revert ✅ + └── when quantity param plus supply claimed is within open claim limit + └── when quantity param plus claimed supply is more than max claimable supply + │ └── it should revert ✅ + └── when quantity param plus claimed supply is within max claimable supply limit + └── when block timestamp is less than start timestamp of claim phase + │ └── it should revert ✅ + └── when block timestamp is greater than or equal to start timestamp of claim phase + └── execution completes -- exit function ✅ + +├── when allowlist but incorrect merkle proof + └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when currency param is equal to open claim currency + └── when pricePerToken param not equal to open claim price + │ └── it should revert ✅ + └── when pricePerToken param is equal to open claim price + └── when quantity param is 0 + │ └── it should revert ✅ + └── when quantity param is not 0 + └── when quantity param plus supply claimed is more than open claim limit + │ └── it should revert ✅ + +├── when allowlist and correct merkle proof + └── when allowlist price is default max uint256 and allowlist currency is default address(0) + │ └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when allowlist price is default max uint256 and allowlist currency is not default + │ └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when allowlist price is not default and allowlist currency is default address(0) + │ └── when currency param not equal to open claim currency + │ └── it should revert ✅ + └── when allowlist price is not default and allowlist currency is not default + │ └── when currency param not equal to allowlist claim currency + │ └── it should revert ✅ + └── when allowlist quantity is default 0 + │ └── when nonzero quantity param plus supply claimed is more than open claim limit + │ └── it should revert ✅ + └── when allowlist quantity is not default + │ └── when nonzero quantity param plus supply claimed is more than allowlist claim limit + │ └── it should revert ✅ + └── when allowlist price is default max uint256 + │ └── when pricePerToken param not equal to open claim price + │ └── it should revert ✅ + └── when allowlist price is not default + │ └── when pricePerToken param not equal to allowlist claim price + │ └── it should revert ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/lazy-mint/lazy-mint/lazyMint.t.sol b/src/test/sdk/extension/upgradeable/lazy-mint/lazy-mint/lazyMint.t.sol new file mode 100644 index 000000000..97fec025d --- /dev/null +++ b/src/test/sdk/extension/upgradeable/lazy-mint/lazy-mint/lazyMint.t.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { LazyMint, BatchMintMetadata } from "contracts/extension/upgradeable/LazyMint.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyLazyMintUpg is LazyMint { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _canLazyMint() internal view override returns (bool) { + return msg.sender == admin; + } + + function getBaseURI(uint256 _tokenId) external view returns (string memory) { + return _getBaseURI(_tokenId); + } + + function getBatchStartId(uint256 _batchID) public view returns (uint256) { + return _getBatchStartId(_batchID); + } + + function nextTokenIdToMint() public view returns (uint256) { + return nextTokenIdToLazyMint(); + } +} + +contract UpgradeableLazyMint_LazyMint is ExtensionUtilTest { + MyLazyMintUpg internal ext; + uint256 internal startId; + uint256 internal amount; + uint256[] internal batchIds; + address internal admin; + address internal caller; + + event TokensLazyMinted(uint256 indexed startTokenId, uint256 endTokenId, string baseURI, bytes encryptedBaseURI); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + + ext = new MyLazyMintUpg(address(admin)); + + startId = 0; + // mint 5 batches + vm.startPrank(admin); + for (uint256 i = 0; i < 5; i++) { + uint256 _amount = (i + 1) * 10; + uint256 batchId = startId + _amount; + batchIds.push(batchId); + + string memory baseURI = Strings.toString(batchId); + startId = ext.lazyMint(_amount, baseURI, ""); + } + vm.stopPrank(); + } + + function test_lazyMint_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + ext.lazyMint(amount, "", ""); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_lazyMint_zeroAmount() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectRevert("0 amt"); + ext.lazyMint(amount, "", ""); + } + + modifier whenAmountNotZero() { + amount = 50; + _; + } + + function test_lazyMint() public whenCallerAuthorized whenAmountNotZero { + // check previous state + uint256 _nextTokenIdToLazyMintOld = ext.nextTokenIdToMint(); + assertEq(_nextTokenIdToLazyMintOld, batchIds[4]); + + string memory baseURI = "ipfs://baseURI"; + + // lazy mint next batch + vm.prank(address(caller)); + uint256 _batchId = ext.lazyMint(amount, baseURI, ""); + + // check new state + uint256 _batchStartId = ext.getBatchStartId(_batchId); + assertEq(_nextTokenIdToLazyMintOld, _batchStartId); + assertEq(_batchId, _nextTokenIdToLazyMintOld + amount); + for (uint256 i = _batchStartId; i < _batchId; i++) { + assertEq(ext.getBaseURI(i), baseURI); + } + assertEq(ext.nextTokenIdToMint(), _nextTokenIdToLazyMintOld + amount); + } + + function test_lazyMint_event() public whenCallerAuthorized whenAmountNotZero { + string memory baseURI = "ipfs://baseURI"; + uint256 _nextTokenIdToLazyMintOld = ext.nextTokenIdToMint(); + + // lazy mint next batch + vm.prank(address(caller)); + vm.expectEmit(); + emit TokensLazyMinted(_nextTokenIdToLazyMintOld, _nextTokenIdToLazyMintOld + amount - 1, baseURI, ""); + ext.lazyMint(amount, baseURI, ""); + } +} diff --git a/src/test/sdk/extension/upgradeable/lazy-mint/lazy-mint/lazyMint.tree b/src/test/sdk/extension/upgradeable/lazy-mint/lazy-mint/lazyMint.tree new file mode 100644 index 000000000..daf177146 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/lazy-mint/lazy-mint/lazyMint.tree @@ -0,0 +1,17 @@ +lazyMint( + uint256 _amount, + string calldata _baseURIForTokens, + bytes calldata _data +) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when amount to lazy mint is 0 + │ └── it should revert ✅ + └── when amount to lazy mint is not 0 + └── it should save the batch of tokens starting at `nextTokenIdToLazyMint` ✅ + └── it should store batch id equal to the sum of `nextTokenIdToLazyMint` and `_amount` ✅ + └── it should map the new batch id to `_baseURIForTokens` ✅ + └── it should increase `nextTokenIdToLazyMint` by `_amount` ✅ + └── it should return the new `batchId` ✅ + └── it should emit TokensLazyMinted event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/ownable/set-owner/setOwner.t.sol b/src/test/sdk/extension/upgradeable/ownable/set-owner/setOwner.t.sol new file mode 100644 index 000000000..6c938158f --- /dev/null +++ b/src/test/sdk/extension/upgradeable/ownable/set-owner/setOwner.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Ownable, IOwnable } from "contracts/extension/upgradeable/Ownable.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyOwnableUpg is Ownable { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function _canSetOwner() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract UpgradeableOwnable_SetOwner is ExtensionUtilTest { + MyOwnableUpg internal ext; + address internal admin; + address internal caller; + address internal oldOwner; + address internal newOwner; + + event OwnerUpdated(address indexed prevOwner, address indexed newOwner); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + + oldOwner = getActor(2); + newOwner = getActor(3); + + ext = new MyOwnableUpg(address(admin)); + + vm.prank(address(admin)); + ext.setOwner(oldOwner); + + assertEq(oldOwner, ext.owner()); + } + + function test_setOwner_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + ext.setOwner(newOwner); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setOwner() public whenCallerAuthorized { + vm.prank(address(caller)); + ext.setOwner(newOwner); + + assertEq(newOwner, ext.owner()); + } + + function test_setOwner_event() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectEmit(true, true, false, false); + emit OwnerUpdated(oldOwner, newOwner); + ext.setOwner(newOwner); + } +} diff --git a/src/test/sdk/extension/upgradeable/ownable/set-owner/setOwner.tree b/src/test/sdk/extension/upgradeable/ownable/set-owner/setOwner.tree new file mode 100644 index 000000000..9db2c0a70 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/ownable/set-owner/setOwner.tree @@ -0,0 +1,6 @@ +setContractURI(string memory uri) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── it should update owner by replacing old owner with the new owner input ✅ + └── it should emit OwnerUpdated event ✅ \ No newline at end of file diff --git a/src/test/sdk/extension/upgradeable/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol b/src/test/sdk/extension/upgradeable/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol new file mode 100644 index 000000000..e541be5ad --- /dev/null +++ b/src/test/sdk/extension/upgradeable/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Royalty, IRoyalty } from "contracts/extension/upgradeable/Royalty.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyRoyaltyUpg is Royalty { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {} + + function _canSetRoyaltyInfo() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract UpgradeableRoyalty_SetDefaultRoyaltyInfo is ExtensionUtilTest { + MyRoyaltyUpg internal ext; + address internal admin; + address internal caller; + address internal defaultRoyaltyRecipient; + uint256 internal defaultRoyaltyBps; + + event DefaultRoyalty(address indexed newRoyaltyRecipient, uint256 newRoyaltyBps); + + function setUp() public override { + super.setUp(); + + admin = getActor(0); + caller = getActor(1); + defaultRoyaltyRecipient = getActor(2); + + ext = new MyRoyaltyUpg(address(admin)); + } + + function test_setDefaultRoyaltyInfo_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setDefaultRoyaltyInfo_exceedMaxBps() public whenCallerAuthorized { + defaultRoyaltyBps = 10_001; + vm.prank(address(caller)); + vm.expectRevert("Exceeds max bps"); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + modifier whenNotExceedMaxBps() { + defaultRoyaltyBps = 500; + _; + } + + function test_setDefaultRoyaltyInfo() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + + // get default royalty info + (address _recipient, uint16 _royaltyBps) = ext.getDefaultRoyaltyInfo(); + assertEq(_recipient, defaultRoyaltyRecipient); + assertEq(_royaltyBps, uint16(defaultRoyaltyBps)); + + // get royalty info for token + uint256 tokenId = 0; + (_recipient, _royaltyBps) = ext.getRoyaltyInfoForToken(tokenId); + assertEq(_recipient, defaultRoyaltyRecipient); + assertEq(_royaltyBps, uint16(defaultRoyaltyBps)); + + // royaltyInfo - ERC2981 + uint256 salePrice = 1000; + (address _royaltyRecipient, uint256 _royaltyAmount) = ext.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); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } +} diff --git a/src/test/sdk/extension/upgradeable/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.tree b/src/test/sdk/extension/upgradeable/royalty/set-default-royalty-info/setDefaultRoyaltyInfo.tree new file mode 100644 index 000000000..78a4312de --- /dev/null +++ b/src/test/sdk/extension/upgradeable/royalty/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/sdk/extension/upgradeable/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol b/src/test/sdk/extension/upgradeable/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol new file mode 100644 index 000000000..d28a142ee --- /dev/null +++ b/src/test/sdk/extension/upgradeable/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "@std/Test.sol"; +import "@ds-test/test.sol"; + +import { Royalty, IRoyalty } from "contracts/extension/upgradeable/Royalty.sol"; +import "../../../ExtensionUtilTest.sol"; + +contract MyRoyaltyUpg is Royalty { + address admin; + + constructor(address _admin) { + admin = _admin; + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {} + + function _canSetRoyaltyInfo() internal view override returns (bool) { + return msg.sender == admin; + } +} + +contract UpgradeableRoyalty_SetRoyaltyInfoForToken is ExtensionUtilTest { + MyRoyaltyUpg internal ext; + address internal admin; + address internal caller; + address internal defaultRoyaltyRecipient; + uint256 internal defaultRoyaltyBps; + + 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(); + + admin = getActor(0); + caller = getActor(1); + defaultRoyaltyRecipient = getActor(2); + royaltyRecipientForToken = getActor(3); + defaultRoyaltyBps = 500; + tokenId = 1; + + ext = new MyRoyaltyUpg(address(admin)); + + vm.prank(address(admin)); + ext.setDefaultRoyaltyInfo(defaultRoyaltyRecipient, defaultRoyaltyBps); + } + + function test_setRoyaltyInfoForToken_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert("Not authorized"); + ext.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } + + modifier whenCallerAuthorized() { + caller = admin; + _; + } + + function test_setRoyaltyInfoForToken_exceedMaxBps() public whenCallerAuthorized { + royaltyBpsForToken = 10_001; + vm.prank(address(caller)); + vm.expectRevert("Exceeds max bps"); + ext.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } + + modifier whenNotExceedMaxBps() { + royaltyBpsForToken = 1000; + _; + } + + function test_setRoyaltyInfoForToken() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + ext.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + + // get default royalty info + (address _defaultRecipient, uint16 _defaultRoyaltyBps) = ext.getDefaultRoyaltyInfo(); + assertEq(_defaultRecipient, defaultRoyaltyRecipient); + assertEq(_defaultRoyaltyBps, uint16(defaultRoyaltyBps)); + + // get royalty info for token + (address _royaltyRecipientForToken, uint16 _royaltyBpsForToken) = ext.getRoyaltyInfoForToken(tokenId); + assertEq(_royaltyRecipientForToken, royaltyRecipientForToken); + assertEq(_royaltyBpsForToken, uint16(royaltyBpsForToken)); + + // royaltyInfo - ERC2981: calculate for default + uint256 salePrice = 1000; + (address _royaltyRecipient, uint256 _royaltyAmount) = ext.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) = ext.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); + ext.setRoyaltyInfoForToken(tokenId, royaltyRecipientForToken, royaltyBpsForToken); + } +} diff --git a/src/test/sdk/extension/upgradeable/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.tree b/src/test/sdk/extension/upgradeable/royalty/set-royalty-info-for-token/setRoyaltyInfoForToken.tree new file mode 100644 index 000000000..e28295634 --- /dev/null +++ b/src/test/sdk/extension/upgradeable/royalty/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 DefaultRoyalty event ✅ \ No newline at end of file diff --git a/src/test/utils/BaseTest.sol b/src/test/utils/BaseTest.sol index eda6c4c20..8bbf83202 100644 --- a/src/test/utils/BaseTest.sol +++ b/src/test/utils/BaseTest.sol @@ -10,6 +10,8 @@ import "../mocks/WETH9.sol"; import "../mocks/MockERC20.sol"; import "../mocks/MockERC721.sol"; import "../mocks/MockERC1155.sol"; +import { MockERC721NonBurnable } from "../mocks/MockERC721NonBurnable.sol"; +import { MockERC1155NonBurnable } from "../mocks/MockERC1155NonBurnable.sol"; import "contracts/infra/forwarder/Forwarder.sol"; import { ForwarderEOAOnly } from "contracts/infra/forwarder/ForwarderEOAOnly.sol"; import "contracts/infra/TWRegistry.sol"; @@ -51,6 +53,8 @@ abstract contract BaseTest is DSTest, Test { MockERC20 public erc20Aux; MockERC721 public erc721; MockERC1155 public erc1155; + MockERC721NonBurnable public erc721NonBurnable; + MockERC1155NonBurnable public erc1155NonBurnable; WETH9 public weth; address public forwarder; @@ -100,6 +104,8 @@ abstract contract BaseTest is DSTest, Test { erc20Aux = new MockERC20(); erc721 = new MockERC721(); erc1155 = new MockERC1155(); + erc721NonBurnable = new MockERC721NonBurnable(); + erc1155NonBurnable = new MockERC1155NonBurnable(); weth = new WETH9(); forwarder = address(new Forwarder()); eoaForwarder = address(new ForwarderEOAOnly());