diff --git a/.env.example b/.env.example index c9a9206..a299b74 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,8 @@ -RPC_URL_MAINNET= -RPC_URL_OPTIMISM= -RPC_URL_BASE= -RPC_URL_BASE_GOERLI= +# Scope: RPC provider credential. +SLICEGLOBAL_INTERNAL_ALCHEMY_ID= +# Scope: explorer verification. ETHERSCAN_KEY= -PRIVATE_KEY= \ No newline at end of file +# Scope: signer keys. +PRIVATE_KEY= diff --git a/.github/workflows/subtree-sync.yml b/.github/workflows/subtree-sync.yml index 8fe9cd9..149ba0a 100644 --- a/.github/workflows/subtree-sync.yml +++ b/.github/workflows/subtree-sync.yml @@ -18,7 +18,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # gh picks this up automatically steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: {fetch-depth: 0} # full history, just in case - name: Ensure PR exists diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 25c3e34..451db1a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,7 +6,7 @@ jobs: tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: recursive diff --git a/LICENSE b/LICENSE index 4bd79c7..ca12056 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -MIT License +The MIT License (MIT) -Copyright (c) 2025 Slice +Copyright (c) Slice Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index a861291..5b3d3f9 100644 --- a/README.md +++ b/README.md @@ -158,4 +158,8 @@ src/ - [Actions Guide](./src/hooks/actions/README.md) - [Pricing Strategies Guide](./src/hooks/pricing/README.md) - [Pricing + Actions Guide](./src/hooks/pricingActions/README.md) -- [Custom Implementations](./src/custom/README.md) \ No newline at end of file +- [Custom Implementations](./src/custom/README.md) + +## License + +This repository is available under the [MIT License](./LICENSE). diff --git a/deployments/addresses.json b/deployments/addresses.json index c3a44b1..3111f86 100644 --- a/deployments/addresses.json +++ b/deployments/addresses.json @@ -2,78 +2,132 @@ "actions": { "Allowlisted": [ { - "address": "0x157428DD791E03c20880D22C3dA2B66A36B5cF26", - "blockNumber": 33510607, - "paramsSchema": "bytes32 merkleRoot", - "transactionHash": "0x6af9ac700f1c9a38de57fa4bc13262162d9b674649fd417ccd50237b6cfbc178" + "address": "0x87f726ac5D1023919840bebde2Da2efb5Da816ad", + "blockNumber": 36441333, + "configureProductSchema": "bytes32 merkleRoot", + "configureProductTypeSchema": "not-supported", + "configureSlicerSchema": "not-supported", + "onProductPurchaseSchema": "", + "productPriceSchema": "not-supported", + "productTypePriceSchema": "not-supported", + "slicerProductPriceSchema": "not-supported", + "transactionHash": "0x57419dee52082559db1d966b1b139bb79ac59c4533de43153dd11fda30b765c1" } ], "ERC20Gated": [ { - "address": "0x26A1C86B555013995Fc72864D261fDe984752E7c", - "blockNumber": 33558792, - "paramsSchema": "(address erc20,uint256 amount)[] erc20Gates", - "transactionHash": "0x3a7c01ede05a34280073479d5cdf1f35e41d9f08c36a71f358b9c503ccc54526" + "address": "0x247ebf608A0028DE933f1F90eE3920864D3fC621", + "blockNumber": 36440973, + "configureProductSchema": "(address erc20,uint256 amount)[] erc20Gates", + "configureProductTypeSchema": "(address erc20,uint256 amount)[] erc20Gates", + "configureSlicerSchema": "(address erc20,uint256 amount)[] erc20Gates", + "onProductPurchaseSchema": "", + "productPriceSchema": "not-supported", + "productTypePriceSchema": "not-supported", + "slicerProductPriceSchema": "not-supported", + "transactionHash": "0xac40db0f5c63ec520e671db10b018018900a332d563c99bc9828a6cc20b48863" } ], "ERC20Mint": [ { - "address": "0x67f9799FaC1D53C63217BEE47f553150F5BB0836", - "blockNumber": 33520592, - "paramsSchema": "string name,string symbol,uint256 premintAmount,address premintReceiver,bool revertOnMaxSupplyReached,uint256 maxSupply,uint256 tokensPerUnit", - "transactionHash": "0xfcd7e8fe47aa509afa0acdef86b741656c2602626f3258f8a83b359c665d6b04" + "address": "0x8B03B4dE7F6FD7A7BDD60Cb00aa49E144e45cdd0", + "blockNumber": 36441450, + "configureProductSchema": "string name,string symbol,uint256 premintAmount,address premintReceiver,bool revertOnMaxSupplyReached,uint256 maxSupply,uint256 tokensPerUnit", + "configureProductTypeSchema": "string name,string symbol,uint256 premintAmount,address premintReceiver,bool revertOnMaxSupplyReached,uint256 maxSupply,uint256 tokensPerUnit", + "configureSlicerSchema": "string name,string symbol,uint256 premintAmount,address premintReceiver,bool revertOnMaxSupplyReached,uint256 maxSupply,uint256 tokensPerUnit", + "onProductPurchaseSchema": "", + "productPriceSchema": "not-supported", + "productTypePriceSchema": "not-supported", + "slicerProductPriceSchema": "not-supported", + "transactionHash": "0xfd77332c2aba9e05a71419b87ddb0a136f85675f7d0f13cf7c46c3e65c99f44c" } ], "ERC721Mint": [ { - "address": "0x2b6488115FAa50142E140172CbCd60e6370675F7", - "blockNumber": 33511082, - "paramsSchema": "string name,string symbol,address royaltyReceiver,uint256 royaltyFraction,string baseURI,string tokenURI,bool revertOnMaxSupplyReached,uint256 maxSupply", - "transactionHash": "0x4981b3b67c0b8abe6a942b3ca86643f2b1dfdbcce59d6c819ad20a82814956e0" + "address": "0x48eAf12a1dC2a6dfACcb4C03F381B07ad5D5961C", + "blockNumber": 36441490, + "configureProductSchema": "string name,string symbol,address royaltyReceiver,uint256 royaltyFraction,string baseURI,string tokenURI,bool revertOnMaxSupplyReached,uint256 maxSupply", + "configureProductTypeSchema": "string name,string symbol,address royaltyReceiver,uint256 royaltyFraction,string baseURI,string tokenURI,bool revertOnMaxSupplyReached,uint256 maxSupply", + "configureSlicerSchema": "string name,string symbol,address royaltyReceiver,uint256 royaltyFraction,string baseURI,string tokenURI,bool revertOnMaxSupplyReached,uint256 maxSupply", + "onProductPurchaseSchema": "", + "productPriceSchema": "not-supported", + "productTypePriceSchema": "not-supported", + "slicerProductPriceSchema": "not-supported", + "transactionHash": "0x4851d2daedfa5de5a6a60a44fb9709e8c865039660c373d6d96f06c4ec2369f5" } ], "NFTGated": [ { - "address": "0xD4eF7A46bF4c58036eaCA886119F5230e5a2C25d", - "blockNumber": 33563508, - "paramsSchema": "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned", - "transactionHash": "0x9dd101a6155b432849cb33f65c5d4b9f6875e6b1e7bf806005a8a6b0d070f634" + "address": "0x560463f7bF3DFDB4Cff4f96c3399313d610Dd349", + "blockNumber": 36441510, + "configureProductSchema": "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] gates,uint256 minOwned", + "configureProductTypeSchema": "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] gates,uint256 minOwned", + "configureSlicerSchema": "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] gates,uint256 minOwned", + "onProductPurchaseSchema": "", + "productPriceSchema": "not-supported", + "productTypePriceSchema": "not-supported", + "slicerProductPriceSchema": "not-supported", + "transactionHash": "0x3bef5e07d303ab1028a7928f9ef1ffb1613b957aff4bfe481ae79a6fd1029e2d" } ] }, "pricing": { - "NFTDiscount": [ + "LinearVRGDAPrices": [ { - "address": "0xb830a457d2f51d4cA1136b97FB30DF6366CFe2f5", - "blockNumber": 33596708, - "paramsSchema": "(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[] discounts", - "transactionHash": "0x966be5fa1da3fab7c5027c9acb3f118307997e30687188b4745d74fd26ad9e7e" + "address": "0x67AEdfe386a078DF350D4b1E033AB735c1fCD0C9", + "blockNumber": 36441538, + "configureProductSchema": "int184 priceDecayPercent,uint16 min,int256 perTimeUnit", + "configureProductTypeSchema": "not-supported", + "configureSlicerSchema": "not-supported", + "onProductPurchaseSchema": "not-supported", + "productPriceSchema": "", + "productTypePriceSchema": "not-supported", + "slicerProductPriceSchema": "not-supported", + "transactionHash": "0xd5ad41a2f994e0151edaf566e8114b703fa1843c4bbd4a8ed79b4312e22a4843" } ], - "LinearVRGDAPrices": [ + "LogisticVRGDAPrices": [ { - "address": "0xEC68E30182F4298b7032400B7ce809da613e4449", - "blockNumber": 33511188, - "paramsSchema": "(address currency,int128 targetPrice,uint128 min,int256 perTimeUnit)[] linearParams,int256 priceDecayPercent", - "transactionHash": "0xf2667ce20d07561e59c8d3e3de135bbd895c5b08bb57fe487c6c8197c91a0d73" + "address": "0x12b56C70fd76D45b578C50aD2eDB582b44d75cC1", + "blockNumber": 36441564, + "configureProductSchema": "int184 priceDecayPercent,uint16 min,int256 timescale", + "configureProductTypeSchema": "not-supported", + "configureSlicerSchema": "not-supported", + "onProductPurchaseSchema": "not-supported", + "productPriceSchema": "", + "productTypePriceSchema": "not-supported", + "slicerProductPriceSchema": "not-supported", + "transactionHash": "0x72fbbdd585b2fd5a2c5be0431e14c0ddee9a266f0c2c6d259dfc9b76a938122a" } ], - "LogisticVRGDAPrices": [ + "NFTDiscount": [ { - "address": "0x2b02cC8528EF18abf8185543CEC29A94F0542c8F", - "blockNumber": 33511209, - "paramsSchema": "(address currency,int128 targetPrice,uint128 min,int256 timeScale)[] logisticParams,int256 priceDecayPercent", - "transactionHash": "0x2b27380661a38b54dbccf634305622296034ddfccf5865a07fbfb7810ea41025" + "address": "0xD92820f4836b23DCe9f7696b12a5C877ec667128", + "blockNumber": 36441359, + "configureProductSchema": "(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[] discounts", + "configureProductTypeSchema": "(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[] discounts", + "configureSlicerSchema": "(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[] discounts", + "onProductPurchaseSchema": "not-supported", + "productPriceSchema": "", + "productTypePriceSchema": "", + "slicerProductPriceSchema": "", + "transactionHash": "0xaf7bb92631cfa26e513b23bd48d60535244a9161e497d25d55ba139890414e3c" } ] }, "pricingActions": { "FirstForFree": [ { - "address": "0x2C18D37b8229233F672bF406bCe8799BCfD43B5A", - "blockNumber": 33510960, - "paramsSchema": "uint256 usdcPrice,(address tokenAddress,uint8 tokenType,uint88 tokenId,uint8 minQuantity)[] eligibleTokens,address mintToken,uint88 mintTokenId,uint8 freeUnits", - "transactionHash": "0x8bd0b9de8edd704899210f8450096adf7f4f853030a7ba0a2f64494bc1ba726e" + "address": "0x718308153116e90393804ae7a58a8e7d5BDb6482", + "blockNumber": 36441582, + "configureProductSchema": "(address tokenAddress,uint8 tokenType,uint80 tokenId,uint8 minQuantity)[] eligibleTokens,address mintToken,uint88 mintTokenId,uint8 freeUnits", + "configureProductTypeSchema": "not-supported", + "configureSlicerSchema": "not-supported", + "onProductPurchaseSchema": "", + "productPriceSchema": "", + "productTypePriceSchema": "not-supported", + "slicerProductPriceSchema": "not-supported", + "transactionHash": "0x59033c01e7228c3f28d25ce233bb729e595515923078498e726d052f038a87aa" } ] } diff --git a/foundry.toml b/foundry.toml index 0f37899..dfc088d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,38 +5,44 @@ optimizer_runs = 1000000 no_match_test = "FFI" dynamic_test_linking = true libs = ["dependencies", "../core/src", "../core/dependencies"] -fs_permissions = [{ access = "read", path = "./src"}, { access= "read", path = "./broadcast/Deploy.s.sol/8453/run-latest.json"}, { access = "read-write", path = "./deployments"}, { access = "read", path = "./out"}] +ignored_warnings_from = ["dependencies", "../core/dependencies"] +fs_permissions = [ + { access = "read", path = "./src" }, + { access = "read", path = "./broadcast/Deploy.s.sol/8453/run-latest.json" }, + { access = "read-write", path = "./deployments" }, + { access = "read", path = "./out" }, +] remappings = [ "slice/=dependencies/slice-0.0.9/", "@openzeppelin-4.8.0/=dependencies/@openzeppelin-contracts-4.8.0/", "@openzeppelin-upgradeable-4.8.0/=dependencies/@openzeppelin-contracts-upgradeable-4.8.0/", + "solady-0.1.26/=dependencies/solady-0.1.26/", "@erc721a/=dependencies/erc721a-4.3.0/contracts/", "forge-std/=dependencies/forge-std-1.9.7/src/", "@test/=test/", - "@/=src/" + "@/=src/", ] [profile.ci] -fuzz_runs = 10000 +fuzz.runs = 10000 [profile.ffi] ffi = true match_test = "FFI" no_match_test = "a^" -fuzz_runs = 1000 +fuzz.runs = 1000 [rpc_endpoints] -mainnet = "${RPC_URL_MAINNET}" -op = "${RPC_URL_OPTIMISM}" -base = "${RPC_URL_BASE}" -base-goerli = "${RPC_URL_BASE_GOERLI}" +mainnet = "https://eth-mainnet.g.alchemy.com/v2/${SLICEGLOBAL_INTERNAL_ALCHEMY_ID}" +op = "https://opt-mainnet.g.alchemy.com/v2/${SLICEGLOBAL_INTERNAL_ALCHEMY_ID}" +base = "https://base-mainnet.g.alchemy.com/v2/${SLICEGLOBAL_INTERNAL_ALCHEMY_ID}" [etherscan] -mainnet = { key="${ETHERSCAN_KEY}", chain=1 } -op = { key="${ETHERSCAN_KEY}", chain=10 } -base = { key="${ETHERSCAN_KEY}", chain=8453, url="https://api.basescan.org/api" } -base-goerli = {key="${ETHERSCAN_KEY}", chain=84531, url="https://api-goerli.basescan.org/api" } +mainnet = { key = "${ETHERSCAN_KEY}", chain = 1 } +op = { key = "${ETHERSCAN_KEY}", chain = 10 } +base = { key = "${ETHERSCAN_KEY}", chain = 8453, url = "https://api.basescan.org/api" } +base-goerli = { key = "${ETHERSCAN_KEY}", chain = 84531, url = "https://api-goerli.basescan.org/api" } [soldeer] recursive_deps = true @@ -49,7 +55,24 @@ forge-std = "1.9.7" "@openzeppelin-contracts" = "4.8.0" "@openzeppelin-contracts-upgradeable" = "4.8.0" erc721a = "4.3.0" +solady = "0.1.26" [lint] -ignore = ["test/**/*.sol","script/**/*.sol", "src/interfaces/*.sol", "src/utils/*.sol", "src/hooks/actions/actions.sol", "src/hooks/pricing/pricing.sol", "src/hooks/pricingActions/pricingActions.sol"] -exclude_lints=["mixed-case-function", "mixed-case-variable", "pascal-case-struct"] +ignore = [ + "dependencies/**/*.sol", + "../core/dependencies/**/*.sol", + "test/**/*.sol", + "script/**/*.sol", + "src/interfaces/*.sol", + "src/utils/*.sol", + "src/hooks/actions/actions.sol", + "src/hooks/pricing/pricing.sol", + "src/hooks/pricingActions/pricingActions.sol", + "src/utils/math/SignedWadMath.sol", +] +exclude_lints = [ + "block-timestamp", + "mixed-case-function", + "mixed-case-variable", + "pascal-case-struct", +] diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 184c75f..3af7b9b 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import {console} from "forge-std/console.sol"; import {VmSafe} from "forge-std/Vm.sol"; import {BaseScript, SetUpContractsList} from "./ScriptUtils.sol"; diff --git a/script/ScriptUtils.sol b/script/ScriptUtils.sol index b65a1d5..3976d91 100644 --- a/script/ScriptUtils.sol +++ b/script/ScriptUtils.sol @@ -7,7 +7,15 @@ import {VmSafe} from "forge-std/Vm.sol"; import {ISliceCore} from "slice/interfaces/ISliceCore.sol"; import {IProductsModule} from "slice/interfaces/IProductsModule.sol"; import {IFundsModule} from "slice/interfaces/IFundsModule.sol"; -import {IHookRegistry} from "slice/interfaces/hooks/IHookRegistry.sol"; +import { + IProductHookRegistry, + IProductTypeHookRegistry, + ISlicerHookRegistry, + IProductPrice, + IProductTypePrice, + ISlicerProductPrice, + IProductAction +} from "slice/interfaces.sol"; /** * Helper contract to enforce correct chain selection in scripts @@ -85,7 +93,12 @@ abstract contract SetUpContractsList is Script { struct ContractDeploymentData { address contractAddress; uint256 blockNumber; - string paramsSchema; + // TODO: Add other params to the json? such as configure[ProductType/Slicer]Schema and [product/productType/slicer]PriceSchema + string configureProductSchema; + string configureProductTypeSchema; + string configureSlicerSchema; + string onProductPurchaseSchema; + string productPriceSchema; bytes32 transactionHash; } @@ -134,9 +147,7 @@ abstract contract SetUpContractsList is Script { string[] memory arrStrings = new string[](existingData.length); for (uint256 j = 0; j < existingData.length; j++) { string memory idx = vm.toString(j); - vm.serializeAddress(idx, "address", existingData[j].contractAddress); - vm.serializeUint(idx, "blockNumber", existingData[j].blockNumber); - vm.serializeString(idx, "paramsSchema", existingData[j].paramsSchema); + _setFields(idx, existingData[j]); arrStrings[j] = vm.serializeBytes32(idx, "transactionHash", existingData[j].transactionHash); } @@ -198,6 +209,89 @@ abstract contract SetUpContractsList is Script { vm.writeJson(vm.serializeString("addresses", firstFolder, updatedGroupJson), ADDRESSES_PATH); } + function _setFields(string memory idx, ContractDeploymentData memory existingData) internal { + vm.serializeAddress(idx, "address", existingData.contractAddress); + vm.serializeUint(idx, "blockNumber", existingData.blockNumber); + vm.serializeString(idx, "configureProductSchema", existingData.configureProductSchema); + + // Keep the previously stored schema values to avoid overwriting valid data when reserializing + vm.serializeString(idx, "configureProductTypeSchema", existingData.configureProductTypeSchema); + vm.serializeString(idx, "configureSlicerSchema", existingData.configureSlicerSchema); + vm.serializeString(idx, "onProductPurchaseSchema", existingData.onProductPurchaseSchema); + vm.serializeString(idx, "productPriceSchema", existingData.productPriceSchema); + } + + function _isProductTypeHookRegistry(address contractAddress) internal view returns (bool) { + return IProductTypeHookRegistry(contractAddress).supportsInterface(type(IProductTypeHookRegistry).interfaceId); + } + + function _isSlicerHookRegistry(address contractAddress) internal view returns (bool) { + return ISlicerHookRegistry(contractAddress).supportsInterface(type(ISlicerHookRegistry).interfaceId); + } + + function _isProductPrice(address contractAddress) internal view returns (bool) { + return IProductPrice(contractAddress).supportsInterface(type(IProductPrice).interfaceId); + } + + function _isProductTypePrice(address contractAddress) internal view returns (bool) { + return IProductTypePrice(contractAddress).supportsInterface(type(IProductTypePrice).interfaceId); + } + + function _isSlicerProductPrice(address contractAddress) internal view returns (bool) { + return ISlicerProductPrice(contractAddress).supportsInterface(type(ISlicerProductPrice).interfaceId); + } + + function _isProductAction(address contractAddress) internal view returns (bool) { + return IProductAction(contractAddress).supportsInterface(type(IProductAction).interfaceId); + } + + function _serializeObject(string[] memory json, Tx1559 memory transaction, Receipt memory receipt) + internal + returns (string[] memory) + { + vm.serializeAddress("0", "address", transaction.contractAddress); + vm.serializeUint("0", "blockNumber", receipt.blockNumber); + vm.serializeString( + "0", "configureProductSchema", IProductHookRegistry(transaction.contractAddress).configureProductSchema() + ); + + vm.serializeString( + "0", + "configureProductTypeSchema", + _isProductTypeHookRegistry(transaction.contractAddress) + ? IProductTypeHookRegistry(transaction.contractAddress).configureProductTypeSchema() + : "not-supported" + ); + + vm.serializeString( + "0", + "configureSlicerSchema", + _isSlicerHookRegistry(transaction.contractAddress) + ? ISlicerHookRegistry(transaction.contractAddress).configureSlicerSchema() + : "not-supported" + ); + + vm.serializeString( + "0", + "onProductPurchaseSchema", + _isProductAction(transaction.contractAddress) + ? IProductAction(transaction.contractAddress).onProductPurchaseSchema() + : "not-supported" + ); + + vm.serializeString( + "0", + "productPriceSchema", + _isProductPrice(transaction.contractAddress) + ? IProductPrice(transaction.contractAddress).productPriceSchema() + : "not-supported" + ); + + json[0] = vm.serializeBytes32("0", "transactionHash", transaction.hash); + + return json; + } + function _buildJsonArray( string memory existingAddresses, string memory key, @@ -210,27 +304,17 @@ abstract contract SetUpContractsList is Script { ContractDeploymentData[] memory existingContractAddresses = abi.decode(contractAddressesJson, (ContractDeploymentData[])); - json = new string[](existingContractAddresses.length + 1); - vm.serializeAddress("0", "address", transaction.contractAddress); - vm.serializeUint("0", "blockNumber", receipt.blockNumber); - vm.serializeString("0", "paramsSchema", IHookRegistry(transaction.contractAddress).paramsSchema()); - json[0] = vm.serializeBytes32("0", "transactionHash", transaction.hash); + json = _serializeObject(new string[](existingContractAddresses.length + 1), transaction, receipt); for (uint256 i = 0; i < existingContractAddresses.length; i++) { ContractDeploymentData memory existingContractAddress = existingContractAddresses[i]; string memory index = vm.toString(i + 1); - vm.serializeAddress(index, "address", existingContractAddress.contractAddress); - vm.serializeUint(index, "blockNumber", existingContractAddress.blockNumber); - vm.serializeString(index, "paramsSchema", existingContractAddress.paramsSchema); + _setFields(index, existingContractAddress); json[i + 1] = vm.serializeBytes32(index, "transactionHash", existingContractAddress.transactionHash); } } else { - json = new string[](1); - vm.serializeAddress("0", "address", transaction.contractAddress); - vm.serializeUint("0", "blockNumber", receipt.blockNumber); - vm.serializeString("0", "paramsSchema", IHookRegistry(transaction.contractAddress).paramsSchema()); - json[0] = vm.serializeBytes32("0", "transactionHash", transaction.hash); + json = _serializeObject(new string[](1), transaction, receipt); } } diff --git a/script/Seed.s.sol b/script/Seed.s.sol index 7cc035a..208c591 100644 --- a/script/Seed.s.sol +++ b/script/Seed.s.sol @@ -1,15 +1,15 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import {Script} from "forge-std/Script.sol"; -import {CommonStorage} from "slice/utils/CommonStorage.sol"; +import {Contracts} from "slice/utils/Contracts.sol"; import {Allowlisted, ERC20Gated, ERC20Mint, ERC721Mint, NFTGated} from "../src/hooks/actions/actions.sol"; import {NFTDiscount, LinearVRGDAPrices, LogisticVRGDAPrices} from "../src/hooks/pricing/pricing.sol"; import {FirstForFree} from "../src/hooks/pricingActions/pricingActions.sol"; // Script to seed the hooks contracts -contract SeedHooksScript is Script, CommonStorage { +contract SeedHooksScript is Script { struct Hook { address hookAddress; bytes code; @@ -23,42 +23,43 @@ contract SeedHooksScript is Script, CommonStorage { function run() external { vm.startBroadcast(); + string memory json = vm.readFile("./deployments/addresses.json"); Hook[] memory hooks = new Hook[](9); hooks[0] = Hook({ - hookAddress: 0x157428DD791E03c20880D22C3dA2B66A36B5cF26, - code: address(new Allowlisted(PRODUCTS_MODULE())).code + hookAddress: vm.parseJsonAddress(json, ".actions.Allowlisted.[0].address"), + code: address(new Allowlisted(Contracts.PRODUCTS_MODULE)).code }); hooks[1] = Hook({ - hookAddress: 0x26A1C86B555013995Fc72864D261fDe984752E7c, - code: address(new ERC20Gated(PRODUCTS_MODULE())).code + hookAddress: vm.parseJsonAddress(json, ".actions.ERC20Gated.[0].address"), + code: address(new ERC20Gated(Contracts.PRODUCTS_MODULE)).code }); hooks[2] = Hook({ - hookAddress: 0x67f9799FaC1D53C63217BEE47f553150F5BB0836, - code: address(new ERC20Mint(PRODUCTS_MODULE())).code + hookAddress: vm.parseJsonAddress(json, ".actions.ERC20Mint.[0].address"), + code: address(new ERC20Mint(Contracts.PRODUCTS_MODULE)).code }); hooks[3] = Hook({ - hookAddress: 0x2b6488115FAa50142E140172CbCd60e6370675F7, - code: address(new ERC721Mint(PRODUCTS_MODULE())).code + hookAddress: vm.parseJsonAddress(json, ".actions.ERC721Mint.[0].address"), + code: address(new ERC721Mint(Contracts.PRODUCTS_MODULE)).code }); hooks[4] = Hook({ - hookAddress: 0xD4eF7A46bF4c58036eaCA886119F5230e5a2C25d, - code: address(new NFTGated(PRODUCTS_MODULE())).code + hookAddress: vm.parseJsonAddress(json, ".actions.NFTGated.[0].address"), + code: address(new NFTGated(Contracts.PRODUCTS_MODULE)).code }); hooks[5] = Hook({ - hookAddress: 0xb830a457d2f51d4cA1136b97FB30DF6366CFe2f5, - code: address(new NFTDiscount(PRODUCTS_MODULE())).code + hookAddress: vm.parseJsonAddress(json, ".pricing.NFTDiscount.[0].address"), + code: address(new NFTDiscount(Contracts.PRODUCTS_MODULE)).code }); hooks[6] = Hook({ - hookAddress: 0xEC68E30182F4298b7032400B7ce809da613e4449, - code: address(new LinearVRGDAPrices(PRODUCTS_MODULE())).code + hookAddress: vm.parseJsonAddress(json, ".pricing.LinearVRGDAPrices.[0].address"), + code: address(new LinearVRGDAPrices(Contracts.PRODUCTS_MODULE)).code }); hooks[7] = Hook({ - hookAddress: 0x2b02cC8528EF18abf8185543CEC29A94F0542c8F, - code: address(new LogisticVRGDAPrices(PRODUCTS_MODULE())).code + hookAddress: vm.parseJsonAddress(json, ".pricing.LogisticVRGDAPrices.[0].address"), + code: address(new LogisticVRGDAPrices(Contracts.PRODUCTS_MODULE)).code }); hooks[8] = Hook({ - hookAddress: 0xEC68E30182F4298b7032400B7ce809da613e4449, - code: address(new FirstForFree(PRODUCTS_MODULE())).code + hookAddress: vm.parseJsonAddress(json, ".pricingActions.FirstForFree.[0].address"), + code: address(new FirstForFree(Contracts.PRODUCTS_MODULE)).code }); // Deploy hooks diff --git a/script/generate-hook.sh b/script/generate-hook.sh index 5b3fe30..7ad1914 100755 --- a/script/generate-hook.sh +++ b/script/generate-hook.sh @@ -165,10 +165,10 @@ pragma solidity ^0.8.30; import { RegistryProductAction, - HookRegistry, + ProductHookRegistry, IProductsModule, IProductAction, - IHookRegistry + IProductHookRegistry } from "@/utils/RegistryProductAction.sol"; /** @@ -193,15 +193,15 @@ contract CONTRACT_NAME is RegistryProductAction { function isPurchaseAllowed( uint256 slicerId, uint256 productId, - address account, + uint256 variantId, + address buyer, uint256 quantity, - bytes memory slicerCustomData, - bytes memory buyerCustomData - ) public view override returns (bool) { + bytes memory data + ) public view override returns (bool isAllowed) { // Your eligibility logic. Return true if eligible, false otherwise. // Returns true by default. - return true; + isAllowed = true; } /** @@ -210,25 +210,25 @@ contract CONTRACT_NAME is RegistryProductAction { function _onProductPurchase( uint256 slicerId, uint256 productId, + uint256 variantId, address buyer, uint256 quantity, - bytes memory slicerCustomData, - bytes memory buyerCustomData + bytes memory data ) internal override { // Your logic to be executed after product purchase. } /** - * @inheritdoc HookRegistry + * @inheritdoc ProductHookRegistry */ - function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { - // Decode params according to `paramsSchema` and store any data required for your logic. + function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) internal override { + // Decode params according to `configureProductSchema` and store any data required for your logic. } /** - * @inheritdoc IHookRegistry + * @inheritdoc IProductHookRegistry */ - function paramsSchema() external pure override returns (string memory) { + function configureProductSchema() public pure override returns (string memory) { // Define the schema for the parameters that will be passed to `_configureProduct`. return ""; } @@ -266,11 +266,11 @@ contract CONTRACT_NAME is ProductAction { function isPurchaseAllowed( uint256 slicerId, uint256 productId, + uint256 variantId, address buyer, uint256 quantity, - bytes memory slicerCustomData, - bytes memory buyerCustomData - ) public view override returns (bool) { + bytes memory data + ) public view override returns (bool isAllowed) { // Your eligibility logic. Return true if eligible, false otherwise. return true; } @@ -281,10 +281,10 @@ contract CONTRACT_NAME is ProductAction { function _onProductPurchase( uint256 slicerId, uint256 productId, + uint256 variantId, address buyer, uint256 quantity, - bytes memory slicerCustomData, - bytes memory buyerCustomData + bytes memory data ) internal override { // Your logic to be executed after product purchase. } @@ -298,10 +298,10 @@ pragma solidity ^0.8.30; import { RegistryProductPrice, - HookRegistry, + ProductHookRegistry, IProductsModule, IProductPrice, - IHookRegistry + IProductHookRegistry } from "@/utils/RegistryProductPrice.sol"; /** @@ -326,25 +326,27 @@ contract CONTRACT_NAME is RegistryProductPrice { function productPrice( uint256 slicerId, uint256 productId, + uint256 variantId, address currency, + uint256 basePrice, uint256 quantity, address buyer, bytes memory data - ) public view override returns (uint256 ethPrice, uint256 currencyPrice) { + ) public view override returns (uint256 price) { // Your pricing logic. Calculate and return the total price, depending on the passed quantity. } /** - * @inheritdoc HookRegistry + * @inheritdoc ProductHookRegistry */ - function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { - // Decode params according to `paramsSchema` and store any data required for your pricing logic. + function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) internal override { + // Decode params according to `configureProductSchema` and store any data required for your pricing logic. } /** - * @inheritdoc IHookRegistry + * @inheritdoc IProductHookRegistry */ - function paramsSchema() external pure override returns (string memory) { + function configureProductSchema() public pure override returns (string memory) { // Define the schema for the parameters that will be passed to `_configureProduct`. return ""; } @@ -382,11 +384,13 @@ contract CONTRACT_NAME is ProductPrice { function productPrice( uint256 slicerId, uint256 productId, + uint256 variantId, address currency, + uint256 basePrice, uint256 quantity, address buyer, bytes memory data - ) external view returns (uint256 ethPrice, uint256 currencyPrice) { + ) external view returns (uint256 price) { // Your pricing logic. Calculate and return the total price. } } @@ -400,12 +404,14 @@ pragma solidity ^0.8.30; import { RegistryProductPriceAction, RegistryProductAction, - HookRegistry, IProductsModule, IProductAction, - IProductPrice, - IHookRegistry + IProductPrice } from "@/utils/RegistryProductPriceAction.sol"; +import { + ProductHookRegistry, + IProductHookRegistry +} from "@/utils/RegistryProductAction.sol"; /** * @title CONTRACT_NAME @@ -429,11 +435,13 @@ contract CONTRACT_NAME is RegistryProductPriceAction { function productPrice( uint256 slicerId, uint256 productId, + uint256 variantId, address currency, + uint256 basePrice, uint256 quantity, address buyer, bytes memory data - ) public view override returns (uint256 ethPrice, uint256 currencyPrice) { + ) public view override returns (uint256 price) { // Your pricing logic. Calculate and return the total price, depending on the passed quantity. } @@ -443,15 +451,15 @@ contract CONTRACT_NAME is RegistryProductPriceAction { function isPurchaseAllowed( uint256 slicerId, uint256 productId, - address account, + uint256 variantId, + address buyer, uint256 quantity, - bytes memory slicerCustomData, - bytes memory buyerCustomData - ) public view override returns (bool) { + bytes memory data + ) public view override returns (bool isAllowed) { // Your eligibility logic. Return true if eligible, false otherwise. // Returns true by default. - return true; + isAllowed = true; } /** @@ -460,25 +468,25 @@ contract CONTRACT_NAME is RegistryProductPriceAction { function _onProductPurchase( uint256 slicerId, uint256 productId, + uint256 variantId, address buyer, uint256 quantity, - bytes memory slicerCustomData, - bytes memory buyerCustomData + bytes memory data ) internal override { // Your logic to be executed after product purchase. } /** - * @inheritdoc HookRegistry + * @inheritdoc ProductHookRegistry */ - function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { - // Decode params according to `paramsSchema` and store any data required for your logic. + function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) internal override { + // Decode params according to `configureProductSchema` and store any data required for your logic. } /** - * @inheritdoc IHookRegistry + * @inheritdoc IProductHookRegistry */ - function paramsSchema() external pure override returns (string memory) { + function configureProductSchema() public pure override returns (string memory) { // Define the schema for the parameters that will be passed to `_configureProduct`. return ""; } @@ -522,11 +530,13 @@ contract CONTRACT_NAME is ProductPriceAction { function productPrice( uint256 slicerId, uint256 productId, + uint256 variantId, address currency, + uint256 basePrice, uint256 quantity, address buyer, bytes memory data - ) external view returns (uint256 ethPrice, uint256 currencyPrice) { + ) external view returns (uint256 price) { // Your pricing logic. Calculate and return the total price. } @@ -536,11 +546,11 @@ contract CONTRACT_NAME is ProductPriceAction { function isPurchaseAllowed( uint256 slicerId, uint256 productId, + uint256 variantId, address buyer, uint256 quantity, - bytes memory slicerCustomData, - bytes memory buyerCustomData - ) public view override returns (bool) { + bytes memory data + ) public view override returns (bool isAllowed) { // Your eligibility logic. Return true if eligible, false otherwise. return true; } @@ -551,10 +561,10 @@ contract CONTRACT_NAME is ProductPriceAction { function _onProductPurchase( uint256 slicerId, uint256 productId, + uint256 variantId, address buyer, uint256 quantity, - bytes memory slicerCustomData, - bytes memory buyerCustomData + bytes memory data ) internal override { // Your logic to be executed after product purchase. } @@ -671,6 +681,7 @@ contract CONTRACT_NAMETest is RegistryProductActionTest { CONTRACT_NAME CONTRACT_VAR; uint256 slicerId = 1; uint256 productId = 1; + uint256 variantId = 0; function setUp() public { CONTRACT_VAR = new CONTRACT_NAME(PRODUCTS_MODULE); @@ -684,6 +695,7 @@ contract CONTRACT_NAMETest is RegistryProductActionTest { CONTRACT_VAR.configureProduct( slicerId, productId, + variantId, abi.encode( // Your params here ) @@ -701,6 +713,7 @@ contract CONTRACT_NAMETest is RegistryProductActionTest { CONTRACT_VAR.configureProduct( slicerId, productId, + variantId, abi.encode( // Your params here ) @@ -708,7 +721,7 @@ contract CONTRACT_NAMETest is RegistryProductActionTest { vm.stopPrank(); - bool isAllowed = CONTRACT_VAR.isPurchaseAllowed(slicerId, productId, buyer, 1, "", ""); + bool isAllowed = CONTRACT_VAR.isPurchaseAllowed(slicerId, productId, variantId, buyer, 1, ""); // Verify isAllowed value based on conditions } @@ -720,6 +733,7 @@ contract CONTRACT_NAMETest is RegistryProductActionTest { CONTRACT_VAR.configureProduct( slicerId, productId, + variantId, abi.encode( // Your params here ) @@ -728,7 +742,7 @@ contract CONTRACT_NAMETest is RegistryProductActionTest { vm.stopPrank(); vm.prank(address(PRODUCTS_MODULE)); - CONTRACT_VAR.onProductPurchase(slicerId, productId, buyer, 1, "", ""); + CONTRACT_VAR.onProductPurchase(slicerId, productId, variantId, buyer, 1, ""); // Verify after purchase logic } @@ -746,6 +760,7 @@ contract CONTRACT_NAMETest is RegistryProductPriceTest { CONTRACT_NAME CONTRACT_VAR; uint256 slicerId = 1; uint256 productId = 1; + uint256 variantId = 0; function setUp() public { CONTRACT_VAR = new CONTRACT_NAME(PRODUCTS_MODULE); @@ -759,6 +774,7 @@ contract CONTRACT_NAMETest is RegistryProductPriceTest { CONTRACT_VAR.configureProduct( slicerId, productId, + variantId, abi.encode( // Your params here ) @@ -776,6 +792,7 @@ contract CONTRACT_NAMETest is RegistryProductPriceTest { CONTRACT_VAR.configureProduct( slicerId, productId, + variantId, abi.encode( // Your params here ) @@ -783,7 +800,7 @@ contract CONTRACT_NAMETest is RegistryProductPriceTest { vm.stopPrank(); - (uint256 ethPrice, uint256 currencyPrice) = CONTRACT_VAR.productPrice(slicerId, productId, ETH, 1, buyer, ""); + (uint256 price) = CONTRACT_VAR.productPrice(slicerId, productId, variantId, ETH, 0, 1, buyer, ""); // Verify product price } @@ -801,6 +818,7 @@ contract CONTRACT_NAMETest is RegistryProductPriceActionTest { CONTRACT_NAME CONTRACT_VAR; uint256 slicerId = 1; uint256 productId = 1; + uint256 variantId = 0; function setUp() public { CONTRACT_VAR = new CONTRACT_NAME(PRODUCTS_MODULE); @@ -814,6 +832,7 @@ contract CONTRACT_NAMETest is RegistryProductPriceActionTest { CONTRACT_VAR.configureProduct( slicerId, productId, + variantId, abi.encode( // Your params here ) @@ -831,6 +850,7 @@ contract CONTRACT_NAMETest is RegistryProductPriceActionTest { CONTRACT_VAR.configureProduct( slicerId, productId, + variantId, abi.encode( // Your params here ) @@ -838,7 +858,7 @@ contract CONTRACT_NAMETest is RegistryProductPriceActionTest { vm.stopPrank(); - (uint256 ethPrice, uint256 currencyPrice) = CONTRACT_VAR.productPrice(slicerId, productId, ETH, 1, buyer, ""); + (uint256 price) = CONTRACT_VAR.productPrice(slicerId, productId, variantId, ETH, 0, 1, buyer, ""); // Verify product price } @@ -850,6 +870,7 @@ contract CONTRACT_NAMETest is RegistryProductPriceActionTest { CONTRACT_VAR.configureProduct( slicerId, productId, + variantId, abi.encode( // Your params here ) @@ -857,7 +878,7 @@ contract CONTRACT_NAMETest is RegistryProductPriceActionTest { vm.stopPrank(); - bool isAllowed = CONTRACT_VAR.isPurchaseAllowed(slicerId, productId, buyer, 1, "", ""); + bool isAllowed = CONTRACT_VAR.isPurchaseAllowed(slicerId, productId, variantId, buyer, 1, ""); // Verify isAllowed value based on conditions } @@ -869,6 +890,7 @@ contract CONTRACT_NAMETest is RegistryProductPriceActionTest { CONTRACT_VAR.configureProduct( slicerId, productId, + variantId, abi.encode( // Your params here ) @@ -877,7 +899,7 @@ contract CONTRACT_NAMETest is RegistryProductPriceActionTest { vm.stopPrank(); vm.prank(address(PRODUCTS_MODULE)); - CONTRACT_VAR.onProductPurchase(slicerId, productId, buyer, 1, "", ""); + CONTRACT_VAR.onProductPurchase(slicerId, productId, variantId, buyer, 1, ""); // Verify after purchase logic } diff --git a/soldeer.lock b/soldeer.lock index 5684e03..fb27390 100644 --- a/soldeer.lock +++ b/soldeer.lock @@ -32,3 +32,10 @@ version = "0.0.9" url = "https://soldeer-revisions.s3.amazonaws.com/slice/0_0_9_28-08-2025_10:52:54_src.zip" checksum = "7fb47bafed059724f2b4a56719eec2e131c231d9f9a8cbcde5d88958b649cd5d" integrity = "7ef2c94c7be0d971da96d01d514903e007f957721da4bc0bd9fba004cebafd20" + +[[dependencies]] +name = "solady" +version = "0.1.26" +url = "https://soldeer-revisions.s3.amazonaws.com/solady/0_1_26_25-08-2025_15:30:06_solady.zip" +checksum = "9872ac7cfd32c1eba32800508a1325c49f4a4aa8c6f670454db91971a583e26b" +integrity = "5da4b5ca9cbad98812a4b75ad528ff34c72a0b84433204be6d1420c81de1d6ff" diff --git a/src/custom/actions/BaseCafe/BaseCafe.sol b/src/custom/actions/BaseCafe/BaseCafe.sol index eae2663..3e2e05c 100644 --- a/src/custom/actions/BaseCafe/BaseCafe.sol +++ b/src/custom/actions/BaseCafe/BaseCafe.sol @@ -32,7 +32,7 @@ contract BaseCafe is ProductAction { * @inheritdoc ProductAction * @notice Mint `quantity` NFTs to `account` on purchase */ - function _onProductPurchase(uint256, uint256, address buyer, uint256 quantity, bytes memory, bytes memory) + function _onProductPurchase(uint256, uint256, uint256, address buyer, uint256 quantity, bytes memory) internal override { diff --git a/src/custom/actions/BaseGirlsScout/BaseGirlsScout.sol b/src/custom/actions/BaseGirlsScout/BaseGirlsScout.sol deleted file mode 100644 index 7588422..0000000 --- a/src/custom/actions/BaseGirlsScout/BaseGirlsScout.sol +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.30; - -import {IProductsModule, ProductAction} from "@/utils/ProductAction.sol"; -import {Ownable} from "@openzeppelin-4.8.0/access/Ownable.sol"; -import {IERC1155} from "@openzeppelin-4.8.0/interfaces/IERC1155.sol"; - -/** - * @title BaseGirlsScout - * @notice Onchain action that mints Base Girls Scout NFTs to the buyer on every purchase. - * @author Slice - */ -contract BaseGirlsScout is ProductAction, Ownable { - /*////////////////////////////////////////////////////////////// - IMMUTABLE STORAGE - //////////////////////////////////////////////////////////////*/ - - ITokenERC1155 public MINT_NFT_COLLECTION = ITokenERC1155(0x7A110890DF5D95CefdB0151143E595b755B7c9b7); - uint256 public MINT_NFT_TOKEN_ID = 1; - - /*////////////////////////////////////////////////////////////// - STORAGE - //////////////////////////////////////////////////////////////*/ - - mapping(uint256 slicerId => bool allowed) public allowedSlicerIds; - - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - constructor(IProductsModule productsModuleAddress, uint256 slicerId) - ProductAction(productsModuleAddress, slicerId) - Ownable() - { - allowedSlicerIds[2217] = true; - allowedSlicerIds[2218] = true; - } - - /*////////////////////////////////////////////////////////////// - FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * @inheritdoc ProductAction - * @notice Mint `quantity` NFTs to `account` on purchase - */ - function _onProductPurchase(uint256, uint256, address buyer, uint256 quantity, bytes memory, bytes memory) - internal - override - { - MINT_NFT_COLLECTION.mintTo(buyer, MINT_NFT_TOKEN_ID, "", quantity); - } - - /*////////////////////////////////////////////////////////////// - FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Called by contract owner to set allowed slicer Ids. - */ - function setAllowedSlicerId(uint256 slicerId, bool allowed) external onlyOwner { - allowedSlicerIds[slicerId] = allowed; - } - - /** - * @notice Called by contract owner to set the mint token collection and token ID. - */ - function setMintTokenId(address collection, uint256 tokenId) external onlyOwner { - MINT_NFT_COLLECTION = ITokenERC1155(collection); - MINT_NFT_TOKEN_ID = tokenId; - } -} - -interface ITokenERC1155 { - function mintTo(address to, uint256 tokenId, string calldata uri, uint256 amount) external; -} diff --git a/src/hooks/actions/Allowlisted/Allowlisted.sol b/src/hooks/actions/Allowlisted/Allowlisted.sol index ab4e539..ce76f0d 100644 --- a/src/hooks/actions/Allowlisted/Allowlisted.sol +++ b/src/hooks/actions/Allowlisted/Allowlisted.sol @@ -3,11 +3,11 @@ pragma solidity ^0.8.30; import {MerkleProof} from "@openzeppelin-4.8.0/utils/cryptography/MerkleProof.sol"; import { - IProductsModule, RegistryProductAction, - HookRegistry, + ProductHookRegistry, + IProductsModule, IProductAction, - IHookRegistry + IProductHookRegistry } from "@/utils/RegistryProductAction.sol"; /** @@ -20,7 +20,8 @@ contract Allowlisted is RegistryProductAction { MUTABLE STORAGE //////////////////////////////////////////////////////////////*/ - mapping(uint256 slicerId => mapping(uint256 productId => bytes32 merkleRoot)) public merkleRoots; + mapping(uint256 slicerId => mapping(uint256 productId => mapping(uint256 variantId => bytes32 merkleRoot))) public + merkleRoots; /*////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -39,13 +40,13 @@ contract Allowlisted is RegistryProductAction { function isPurchaseAllowed( uint256 slicerId, uint256 productId, + uint256 variantId, address buyer, uint256, - bytes memory, - bytes memory buyerCustomData + bytes memory data ) public view override returns (bool isAllowed) { - // Get Merkle proof from buyerCustomData - bytes32[] memory proof = abi.decode(buyerCustomData, (bytes32[])); + // Get Merkle proof from data + bytes32[] memory proof = abi.decode(data, (bytes32[])); uint256 leafValue = uint256(uint160(buyer)); @@ -57,25 +58,28 @@ contract Allowlisted is RegistryProductAction { leaf := keccak256(0x00, 0x20) } - bytes32 root = merkleRoots[slicerId][productId]; + bytes32 root = merkleRoots[slicerId][productId][variantId]; // Check if Merkle proof is valid isAllowed = MerkleProof.verify(proof, root, leaf); } /** - * @inheritdoc HookRegistry + * @inheritdoc ProductHookRegistry * @dev Sets the Merkle root for the allowlist. */ - function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { + function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) + internal + override + { (bytes32 merkleRoot) = abi.decode(params, (bytes32)); - merkleRoots[slicerId][productId] = merkleRoot; + merkleRoots[slicerId][productId][variantId] = merkleRoot; } /** - * @inheritdoc IHookRegistry + * @inheritdoc IProductHookRegistry */ - function paramsSchema() external pure override returns (string memory) { + function configureProductSchema() public pure override returns (string memory) { return "bytes32 merkleRoot"; } } diff --git a/src/hooks/actions/ERC20Gated/ERC20Gated.sol b/src/hooks/actions/ERC20Gated/ERC20Gated.sol index f4964dc..78223a6 100644 --- a/src/hooks/actions/ERC20Gated/ERC20Gated.sol +++ b/src/hooks/actions/ERC20Gated/ERC20Gated.sol @@ -3,30 +3,36 @@ pragma solidity ^0.8.30; import {ERC20Gate} from "./types/ERC20Gate.sol"; import { - IProductsModule, - RegistryProductAction, HookRegistry, + IProductsModule, + RegistryAction, IProductAction, - IHookRegistry -} from "@/utils/RegistryProductAction.sol"; + IProductTypeAction, + ISlicerAction, + ProductLineItem +} from "@/utils/RegistryAction.sol"; +import {ProductHookRegistry, IProductHookRegistry} from "@/utils/RegistryProductAction.sol"; /** * @title ERC20Gated * @notice Onchain action registry for ERC20 gating. * @author Slice */ -contract ERC20Gated is RegistryProductAction { +contract ERC20Gated is RegistryAction { /*////////////////////////////////////////////////////////////// MUTABLE STORAGE //////////////////////////////////////////////////////////////*/ - mapping(uint256 slicerId => mapping(uint256 productId => ERC20Gate[] gates)) public tokenGates; + mapping(uint256 slicerId => mapping(uint256 productId => mapping(uint256 variantId => ERC20Gate[] gates))) public + tokenGates; + mapping(uint256 slicerId => mapping(uint256 productTypeId => ERC20Gate[] gates)) public productTypeTokenGates; + mapping(uint256 slicerId => ERC20Gate[] gates) public slicerTokenGates; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(IProductsModule productsModuleAddress) RegistryProductAction(productsModuleAddress) {} + constructor(IProductsModule productsModuleAddress) RegistryAction(productsModuleAddress) {} /*////////////////////////////////////////////////////////////// CONFIGURATION @@ -36,43 +42,110 @@ contract ERC20Gated is RegistryProductAction { * @inheritdoc IProductAction * @dev Checks if `account` owns the required amount of all ERC20 tokens. */ - function isPurchaseAllowed(uint256 slicerId, uint256 productId, address buyer, uint256, bytes memory, bytes memory) + function isPurchaseAllowed( + uint256 slicerId, + uint256 productId, + uint256 variantId, + address buyer, + uint256, + bytes memory + ) public view override returns (bool) { + ERC20Gate[] memory gates = tokenGates[slicerId][productId][variantId]; + + return handleIsPurchaseAllowed(gates, buyer); + } + + /** + * @inheritdoc IProductTypeAction + * @dev Checks if `account` owns the required amount of all ERC20 tokens. + */ + function isProductTypePurchaseAllowed(uint256 slicerId, uint256 productTypeId, address buyer, uint256) public view override returns (bool) { - ERC20Gate[] memory gates = tokenGates[slicerId][productId]; + ERC20Gate[] memory gates = productTypeTokenGates[slicerId][productTypeId]; - for (uint256 i = 0; i < gates.length; i++) { - ERC20Gate memory gate = gates[i]; - uint256 accountBalance = gate.erc20.balanceOf(buyer); - if (accountBalance < gate.amount) { - return false; - } - } + return handleIsPurchaseAllowed(gates, buyer); + } - return true; + /** + * @inheritdoc ISlicerAction + * @dev Checks if `account` owns the required amount of all ERC20 tokens. + */ + function isSlicerPurchaseAllowed(uint256 slicerId, address buyer, ProductLineItem[] memory) + public + view + override + returns (bool) + { + ERC20Gate[] memory gates = slicerTokenGates[slicerId]; + + return handleIsPurchaseAllowed(gates, buyer); } /** - * @inheritdoc HookRegistry + * @inheritdoc ProductHookRegistry * @dev Sets the ERC20 gates for a product. */ - function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { - (ERC20Gate[] memory gates) = abi.decode(params, (ERC20Gate[])); + function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) + internal + override + { + delete tokenGates[slicerId][productId][variantId]; + + handleConfigure(tokenGates[slicerId][productId][variantId], params); + } - delete tokenGates[slicerId][productId]; + /** + * @inheritdoc HookRegistry + * @dev Sets the ERC20 gates for a product type. + */ + function _configureProductType(uint256 slicerId, uint256 productTypeId, bytes memory params) internal override { + delete productTypeTokenGates[slicerId][productTypeId]; - for (uint256 i = 0; i < gates.length; i++) { - tokenGates[slicerId][productId].push(gates[i]); - } + handleConfigure(productTypeTokenGates[slicerId][productTypeId], params); } /** - * @inheritdoc IHookRegistry + * @inheritdoc HookRegistry + * @dev Sets the ERC20 gates for a slicer. */ - function paramsSchema() external pure override returns (string memory) { + function _configureSlicer(uint256 slicerId, bytes memory params) internal override { + delete slicerTokenGates[slicerId]; + + handleConfigure(slicerTokenGates[slicerId], params); + } + + /** + * @inheritdoc IProductHookRegistry + */ + function configureProductSchema() public pure override returns (string memory) { return "(address erc20,uint256 amount)[] erc20Gates"; } + + /*////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////*/ + function handleConfigure(ERC20Gate[] storage gatesStorage, bytes memory params) internal { + (ERC20Gate[] memory gates) = abi.decode(params, (ERC20Gate[])); + + for (uint256 i = 0; i < gates.length; i++) { + gatesStorage.push(gates[i]); + } + } + + function handleIsPurchaseAllowed(ERC20Gate[] memory gates, address buyer) internal view returns (bool) { + ERC20Gate memory gate; + for (uint256 i = 0; i < gates.length; i++) { + gate = gates[i]; + uint256 accountBalance = gate.erc20.balanceOf(buyer); + if (accountBalance < gate.amount) { + return false; + } + } + + return true; + } } diff --git a/src/hooks/actions/ERC20Mint/ERC20Mint.sol b/src/hooks/actions/ERC20Mint/ERC20Mint.sol index a6ab8d3..c690e56 100644 --- a/src/hooks/actions/ERC20Mint/ERC20Mint.sol +++ b/src/hooks/actions/ERC20Mint/ERC20Mint.sol @@ -2,12 +2,15 @@ pragma solidity ^0.8.30; import { - IProductsModule, - RegistryProductAction, HookRegistry, + IProductsModule, + RegistryAction, IProductAction, - IHookRegistry -} from "@/utils/RegistryProductAction.sol"; + IProductTypeAction, + ISlicerAction, + ProductLineItem +} from "@/utils/RegistryAction.sol"; +import {ProductHookRegistry, IProductHookRegistry} from "@/utils/RegistryProductAction.sol"; import {ERC20Data} from "./types/ERC20Data.sol"; import {ERC20Mint_BaseToken} from "./utils/ERC20Mint_BaseToken.sol"; @@ -17,7 +20,7 @@ import {ERC20Mint_BaseToken} from "./utils/ERC20Mint_BaseToken.sol"; * @dev If `revertOnMaxSupplyReached` is set to true, reverts when max supply is exceeded. * @author Slice */ -contract ERC20Mint is RegistryProductAction { +contract ERC20Mint is RegistryAction { /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ @@ -28,13 +31,16 @@ contract ERC20Mint is RegistryProductAction { MUTABLE STORAGE //////////////////////////////////////////////////////////////*/ - mapping(uint256 slicerId => mapping(uint256 productId => ERC20Data tokenData)) public tokenData; + mapping(uint256 slicerId => mapping(uint256 productId => mapping(uint256 variantId => ERC20Data tokenData))) public + tokenData; + mapping(uint256 slicerId => mapping(uint256 productTypeId => ERC20Data tokenData)) public productTypeTokenData; + mapping(uint256 slicerId => ERC20Data tokenData) public slicerTokenData; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(IProductsModule productsModuleAddress) RegistryProductAction(productsModuleAddress) {} + constructor(IProductsModule productsModuleAddress) RegistryAction(productsModuleAddress) {} /*////////////////////////////////////////////////////////////// CONFIGURATION @@ -47,51 +53,149 @@ contract ERC20Mint is RegistryProductAction { function isPurchaseAllowed( uint256 slicerId, uint256 productId, + uint256 variantId, address, uint256 quantity, - bytes memory, bytes memory ) public view virtual override returns (bool isAllowed) { - ERC20Data memory tokenData_ = tokenData[slicerId][productId]; + ERC20Data memory tokenData_ = tokenData[slicerId][productId][variantId]; - if (tokenData_.revertOnMaxSupplyReached) { - return - tokenData_.token.totalSupply() + (quantity * tokenData_.tokensPerUnit) <= tokenData_.token.maxSupply(); + return handleIsPurchaseAllowed(tokenData_, quantity, false); + } + + /** + * @inheritdoc IProductTypeAction + * @dev Checks if `account` owns the required amount of all ERC20 tokens. + */ + function isProductTypePurchaseAllowed(uint256 slicerId, uint256 productTypeId, address, uint256 quantity) + public + view + override + returns (bool) + { + ERC20Data memory tokenData_ = productTypeTokenData[slicerId][productTypeId]; + + return handleIsPurchaseAllowed(tokenData_, quantity, false); + } + + /** + * @inheritdoc ISlicerAction + * @dev Checks if `account` owns the required amount of all ERC20 tokens. + */ + function isSlicerPurchaseAllowed(uint256 slicerId, address, ProductLineItem[] memory productLineItems) + public + view + override + returns (bool) + { + ERC20Data memory tokenData_ = slicerTokenData[slicerId]; + + uint256 quantity; + for (uint256 i = 0; i < productLineItems.length;) { + quantity += productLineItems[i].quantity; + + unchecked { + ++i; + } } - return true; + return handleIsPurchaseAllowed(tokenData_, quantity, true); } /** - * @inheritdoc RegistryProductAction + * @inheritdoc RegistryAction * @notice Mint tokens to the buyer. * @dev If `revertOnMaxSupplyReached` is set to true, reverts when max supply is exceeded. */ function _onProductPurchase( uint256 slicerId, uint256 productId, + uint256 variantId, address buyer, uint256 quantity, - bytes memory, bytes memory ) internal override { - ERC20Data memory tokenData_ = tokenData[slicerId][productId]; + ERC20Data memory tokenData_ = tokenData[slicerId][productId][variantId]; - uint256 tokensToMint = quantity * tokenData_.tokensPerUnit; + handleOnProductPurchase(tokenData_, buyer, quantity); + } - (bool success,) = - address(tokenData_.token).call(abi.encodeWithSelector(tokenData_.token.mint.selector, buyer, tokensToMint)); + /** + * @inheritdoc RegistryAction + * @notice Mint tokens to the buyer. + * @dev If `revertOnMaxSupplyReached` is set to true, reverts when max supply is exceeded. + */ + function _onProductTypePurchase(uint256 slicerId, uint256 productTypeId, address buyer, uint256 quantity) + internal + override + { + ERC20Data memory tokenData_ = productTypeTokenData[slicerId][productTypeId]; + handleOnProductPurchase(tokenData_, buyer, quantity); + } - if (success) { - // Do nothing, just silence the warning + /** + * @inheritdoc RegistryAction + * @notice Mint tokens to the buyer. + * @dev If `revertOnMaxSupplyReached` is set to true, reverts when max supply is exceeded. + */ + function _onSlicerPurchase(uint256 slicerId, address buyer, ProductLineItem[] memory productLineItems) + internal + override + { + ERC20Data memory tokenData_ = slicerTokenData[slicerId]; + + uint256 quantity; + for (uint256 i = 0; i < productLineItems.length;) { + quantity += productLineItems[i].quantity; + + unchecked { + ++i; + } } + + handleOnProductPurchase(tokenData_, buyer, quantity); } /** - * @inheritdoc HookRegistry + * @inheritdoc ProductHookRegistry * @dev Set the ERC20 data for a product. */ - function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { + function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) + internal + override + { + handleConfigure(tokenData[slicerId][productId][variantId], params); + } + + /** + * @inheritdoc HookRegistry + * @dev Set the ERC20 data for a product type. + */ + function _configureProductType(uint256 slicerId, uint256 productTypeId, bytes memory params) internal override { + handleConfigure(productTypeTokenData[slicerId][productTypeId], params); + } + + /** + * @inheritdoc HookRegistry + * @dev Set the ERC20 data for a slicer. + */ + function _configureSlicer(uint256 slicerId, bytes memory params) internal override { + handleConfigure(slicerTokenData[slicerId], params); + } + + /** + * @inheritdoc IProductHookRegistry + */ + function configureProductSchema() public pure override returns (string memory) { + return + "string name,string symbol,uint256 premintAmount,address premintReceiver,bool revertOnMaxSupplyReached,uint256 maxSupply,uint256 tokensPerUnit"; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////*/ + + function handleConfigure(ERC20Data storage tokenDataStorage, bytes memory params) internal { ( string memory name, string memory symbol, @@ -104,7 +208,7 @@ contract ERC20Mint is RegistryProductAction { if (tokensPerUnit == 0) revert InvalidTokensPerUnit(); - ERC20Mint_BaseToken token = tokenData[slicerId][productId].token; + ERC20Mint_BaseToken token = tokenDataStorage.token; if (address(token) == address(0)) { token = new ERC20Mint_BaseToken(name, symbol, maxSupply); @@ -115,14 +219,32 @@ contract ERC20Mint is RegistryProductAction { token.setMaxSupply(maxSupply); } - tokenData[slicerId][productId] = ERC20Data(token, revertOnMaxSupplyReached, tokensPerUnit); + tokenDataStorage.token = token; + tokenDataStorage.revertOnMaxSupplyReached = revertOnMaxSupplyReached; + tokenDataStorage.tokensPerUnit = tokensPerUnit; } - /** - * @inheritdoc IHookRegistry - */ - function paramsSchema() external pure override returns (string memory) { - return - "string name,string symbol,uint256 premintAmount,address premintReceiver,bool revertOnMaxSupplyReached,uint256 maxSupply,uint256 tokensPerUnit"; + function handleIsPurchaseAllowed(ERC20Data memory tokenData_, uint256 quantity, bool isSlicerAction) + internal + view + returns (bool) + { + if (!isSlicerAction && tokenData_.revertOnMaxSupplyReached) { + return + tokenData_.token.totalSupply() + (quantity * tokenData_.tokensPerUnit) <= tokenData_.token.maxSupply(); + } + + return true; + } + + function handleOnProductPurchase(ERC20Data memory tokenData_, address buyer, uint256 quantity) internal { + uint256 tokensToMint = quantity * tokenData_.tokensPerUnit; + + (bool success,) = + address(tokenData_.token).call(abi.encodeWithSelector(tokenData_.token.mint.selector, buyer, tokensToMint)); + + if (success) { + // Do nothing, just silence the warning + } } } diff --git a/src/hooks/actions/ERC721Mint/ERC721Mint.sol b/src/hooks/actions/ERC721Mint/ERC721Mint.sol index 8d3a18f..de4d04f 100644 --- a/src/hooks/actions/ERC721Mint/ERC721Mint.sol +++ b/src/hooks/actions/ERC721Mint/ERC721Mint.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import {IProductsModule, RegistryProductAction, HookRegistry, IHookRegistry} from "@/utils/RegistryProductAction.sol"; +import {HookRegistry, IProductsModule, RegistryAction, ProductLineItem} from "@/utils/RegistryAction.sol"; +import {ProductHookRegistry, IProductHookRegistry} from "@/utils/RegistryProductAction.sol"; import {MAX_ROYALTY, ERC721Mint_BaseToken} from "./utils/ERC721Mint_BaseToken.sol"; import {ERC721Data} from "./types/ERC721Data.sol"; @@ -11,7 +12,7 @@ import {ERC721Data} from "./types/ERC721Data.sol"; * @dev If `revertOnMaxSupplyReached` is set to true, reverts when max supply is exceeded. * @author Slice */ -contract ERC721Mint is RegistryProductAction { +contract ERC721Mint is RegistryAction { /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ @@ -23,46 +24,114 @@ contract ERC721Mint is RegistryProductAction { MUTABLE STORAGE //////////////////////////////////////////////////////////////*/ - mapping(uint256 slicerId => mapping(uint256 productId => ERC721Data tokenData)) public tokenData; + mapping(uint256 slicerId => mapping(uint256 productId => mapping(uint256 variantId => ERC721Data tokenData))) public + tokenData; + mapping(uint256 slicerId => mapping(uint256 productTypeId => ERC721Data tokenData)) public productTypeTokenData; + mapping(uint256 slicerId => ERC721Data tokenData) public slicerTokenData; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(IProductsModule productsModuleAddress) RegistryProductAction(productsModuleAddress) {} + constructor(IProductsModule productsModuleAddress) RegistryAction(productsModuleAddress) {} /*////////////////////////////////////////////////////////////// CONFIGURATION //////////////////////////////////////////////////////////////*/ /** - * @inheritdoc RegistryProductAction + * @inheritdoc RegistryAction * @notice Mint tokens to the buyer. * @dev If `revertOnMaxSupplyReached` is set to true, reverts when max supply is exceeded. */ function _onProductPurchase( uint256 slicerId, uint256 productId, + uint256 variantId, address buyer, uint256 quantity, - bytes memory, bytes memory ) internal override { - ERC721Data memory tokenData_ = tokenData[slicerId][productId]; + ERC721Data memory tokenData_ = tokenData[slicerId][productId][variantId]; - (bool success,) = - address(tokenData_.token).call(abi.encodeWithSelector(tokenData_.token.mint.selector, buyer, quantity)); + handleOnProductPurchase(tokenData_, buyer, quantity, false); + } - if (tokenData_.revertOnMaxSupplyReached) { - if (!success) revert MaxSupplyExceeded(); + /** + * @inheritdoc RegistryAction + * @notice Mint tokens to the buyer. + * @dev If `revertOnMaxSupplyReached` is set to true, reverts when max supply is exceeded. + */ + function _onProductTypePurchase(uint256 slicerId, uint256 productTypeId, address buyer, uint256 quantity) + internal + override + { + ERC721Data memory tokenData_ = productTypeTokenData[slicerId][productTypeId]; + handleOnProductPurchase(tokenData_, buyer, quantity, false); + } + + /** + * @inheritdoc RegistryAction + * @notice Mint tokens to the buyer. + * @dev If `revertOnMaxSupplyReached` is set to true, reverts when max supply is exceeded. + */ + function _onSlicerPurchase(uint256 slicerId, address buyer, ProductLineItem[] memory productLineItems) + internal + override + { + ERC721Data memory tokenData_ = slicerTokenData[slicerId]; + + uint256 quantity; + for (uint256 i = 0; i < productLineItems.length;) { + quantity += productLineItems[i].quantity; + + unchecked { + ++i; + } } + + handleOnProductPurchase(tokenData_, buyer, quantity, true); } /** - * @inheritdoc HookRegistry + * @inheritdoc ProductHookRegistry * @dev Set the ERC721 data for a product. */ - function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { + function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) + internal + override + { + handleConfigure(tokenData[slicerId][productId][variantId], params); + } + + /** + * @inheritdoc HookRegistry + * @dev Set the ERC721 data for a product type. + */ + function _configureProductType(uint256 slicerId, uint256 productTypeId, bytes memory params) internal override { + handleConfigure(productTypeTokenData[slicerId][productTypeId], params); + } + + /** + * @inheritdoc HookRegistry + * @dev Set the ERC721 data for a slicer. + */ + function _configureSlicer(uint256 slicerId, bytes memory params) internal override { + handleConfigure(slicerTokenData[slicerId], params); + } + + /** + * @inheritdoc IProductHookRegistry + */ + function configureProductSchema() public pure override returns (string memory) { + return + "string name,string symbol,address royaltyReceiver,uint256 royaltyFraction,string baseURI,string tokenURI,bool revertOnMaxSupplyReached,uint256 maxSupply"; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////*/ + function handleConfigure(ERC721Data storage tokenDataStorage, bytes memory params) internal { ( string memory name_, string memory symbol_, @@ -76,7 +145,7 @@ contract ERC721Mint is RegistryProductAction { if (royaltyFraction_ > MAX_ROYALTY) revert InvalidRoyaltyFraction(); - ERC721Mint_BaseToken token = tokenData[slicerId][productId].token; + ERC721Mint_BaseToken token = tokenDataStorage.token; if (address(token) == address(0)) { token = new ERC721Mint_BaseToken( @@ -86,14 +155,18 @@ contract ERC721Mint is RegistryProductAction { token.setParams(maxSupply, royaltyReceiver_, royaltyFraction_, baseURI__, tokenURI__); } - tokenData[slicerId][productId] = ERC721Data(token, revertOnMaxSupplyReached); + tokenDataStorage.token = token; + tokenDataStorage.revertOnMaxSupplyReached = revertOnMaxSupplyReached; } - /** - * @inheritdoc IHookRegistry - */ - function paramsSchema() external pure override returns (string memory) { - return - "string name,string symbol,address royaltyReceiver,uint256 royaltyFraction,string baseURI,string tokenURI,bool revertOnMaxSupplyReached,uint256 maxSupply"; + function handleOnProductPurchase(ERC721Data memory tokenData_, address buyer, uint256 quantity, bool isSlicerAction) + internal + { + (bool success,) = + address(tokenData_.token).call(abi.encodeWithSelector(tokenData_.token.mint.selector, buyer, quantity)); + + if (!isSlicerAction && tokenData_.revertOnMaxSupplyReached) { + if (!success) revert MaxSupplyExceeded(); + } } } diff --git a/src/hooks/actions/NFTGated/NFTGated.sol b/src/hooks/actions/NFTGated/NFTGated.sol index b792296..65a82fb 100644 --- a/src/hooks/actions/NFTGated/NFTGated.sol +++ b/src/hooks/actions/NFTGated/NFTGated.sol @@ -4,12 +4,15 @@ pragma solidity ^0.8.30; import {IERC721} from "@openzeppelin-4.8.0/interfaces/IERC721.sol"; import {IERC1155} from "@openzeppelin-4.8.0/interfaces/IERC1155.sol"; import { - IProductsModule, - RegistryProductAction, HookRegistry, + IProductsModule, + RegistryAction, IProductAction, - IHookRegistry -} from "@/utils/RegistryProductAction.sol"; + IProductTypeAction, + ISlicerAction, + ProductLineItem +} from "@/utils/RegistryAction.sol"; +import {ProductHookRegistry, IProductHookRegistry} from "@/utils/RegistryProductAction.sol"; import {NftType, NFTGate, NFTGates} from "./types/NFTGate.sol"; /** @@ -17,18 +20,23 @@ import {NftType, NFTGate, NFTGates} from "./types/NFTGate.sol"; * @notice Onchain action registry for NFT gating. * @author Slice */ -contract NFTGated is RegistryProductAction { +contract NFTGated is RegistryAction { + error InvalidNFT(); + /*////////////////////////////////////////////////////////////// MUTABLE STORAGE //////////////////////////////////////////////////////////////*/ - mapping(uint256 slicerId => mapping(uint256 productId => NFTGates gates)) public nftGates; + mapping(uint256 slicerId => mapping(uint256 productId => mapping(uint256 variantId => NFTGates gates))) public + nftGates; + mapping(uint256 slicerId => mapping(uint256 productTypeId => NFTGates gates)) public productTypeNftGates; + mapping(uint256 slicerId => NFTGates gates) public slicerNftGates; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(IProductsModule productsModuleAddress) RegistryProductAction(productsModuleAddress) {} + constructor(IProductsModule productsModuleAddress) RegistryAction(productsModuleAddress) {} /*////////////////////////////////////////////////////////////// CONFIGURATION @@ -38,13 +46,105 @@ contract NFTGated is RegistryProductAction { * @inheritdoc IProductAction * @dev Checks if `account` owns the required amount of NFT tokens. */ - function isPurchaseAllowed(uint256 slicerId, uint256 productId, address buyer, uint256, bytes memory, bytes memory) + function isPurchaseAllowed( + uint256 slicerId, + uint256 productId, + uint256 variantId, + address buyer, + uint256, + bytes memory + ) public view override returns (bool isAllowed) { + isAllowed = handleIsPurchaseAllowed(nftGates[slicerId][productId][variantId], buyer); + } + + /** + * @inheritdoc IProductTypeAction + * @dev Checks if `account` owns the required amount of NFT tokens. + */ + function isProductTypePurchaseAllowed(uint256 slicerId, uint256 productTypeId, address buyer, uint256) + public + view + override + returns (bool isAllowed) + { + isAllowed = handleIsPurchaseAllowed(productTypeNftGates[slicerId][productTypeId], buyer); + } + + /** + * @inheritdoc ISlicerAction + * @dev Checks if `account` owns the required amount of NFT tokens. + */ + function isSlicerPurchaseAllowed(uint256 slicerId, address buyer, ProductLineItem[] memory) public view override returns (bool isAllowed) { - NFTGates memory nftGates_ = nftGates[slicerId][productId]; + isAllowed = handleIsPurchaseAllowed(slicerNftGates[slicerId], buyer); + } + + /** + * @inheritdoc ProductHookRegistry + * @dev Set the NFT gates for a product. + */ + function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) + internal + override + { + delete nftGates[slicerId][productId][variantId].gates; + handleConfigure(nftGates[slicerId][productId][variantId], params); + } + + /** + * @inheritdoc HookRegistry + * @dev Set the NFT gates for a product type. + */ + function _configureProductType(uint256 slicerId, uint256 productTypeId, bytes memory params) internal override { + delete productTypeNftGates[slicerId][productTypeId].gates; + handleConfigure(productTypeNftGates[slicerId][productTypeId], params); + } + + /** + * @inheritdoc HookRegistry + * @dev Set the NFT gates for a slicer. + */ + function _configureSlicer(uint256 slicerId, bytes memory params) internal override { + delete slicerNftGates[slicerId].gates; + handleConfigure(slicerNftGates[slicerId], params); + } + + /** + * @inheritdoc IProductHookRegistry + */ + function configureProductSchema() public pure override returns (string memory) { + return "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] gates,uint256 minOwned"; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////*/ + + function handleConfigure(NFTGates storage gatesStorage, bytes memory params) internal { + (NFTGate[] memory gates, uint256 minOwned) = abi.decode(params, (NFTGate[], uint256)); + + gatesStorage.minOwned = minOwned; + for (uint256 i = 0; i < gates.length; i++) { + if (gates[i].nftType == NftType.ERC721) { + if (!IERC721(gates[i].nft).supportsInterface(type(IERC721).interfaceId)) { + revert InvalidNFT(); + } + } else if (!IERC1155(gates[i].nft).supportsInterface(type(IERC1155).interfaceId)) { + revert InvalidNFT(); + } + gatesStorage.gates.push(gates[i]); + } + } + + function handleIsPurchaseAllowed(NFTGates memory nftGates_, address buyer) internal view returns (bool) { + // If no gates, allow purchase + if (nftGates_.gates.length == 0) { + return true; + } uint256 totalOwned; unchecked { @@ -64,27 +164,7 @@ contract NFTGated is RegistryProductAction { ++i; } } - } - /** - * @inheritdoc HookRegistry - * @dev Set the NFT gates for a product. - */ - function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { - (NFTGates memory nftGates_) = abi.decode(params, (NFTGates)); - - delete nftGates[slicerId][productId].gates; - - nftGates[slicerId][productId].minOwned = nftGates_.minOwned; - for (uint256 i = 0; i < nftGates_.gates.length; i++) { - nftGates[slicerId][productId].gates.push(nftGates_.gates[i]); - } - } - - /** - * @inheritdoc IHookRegistry - */ - function paramsSchema() external pure override returns (string memory) { - return "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned"; + return false; } } diff --git a/src/hooks/actions/README.md b/src/hooks/actions/README.md index 4770360..02f3417 100644 --- a/src/hooks/actions/README.md +++ b/src/hooks/actions/README.md @@ -32,7 +32,7 @@ The script will create your contract file with the proper template and add it to Actions inheriting from `RegistryProductAction` automatically support frontend integration through: - **Product configuration** via `configureProduct()` -- **Parameter validation** via `paramsSchema()` +- **Parameter validation** via `configureProductSchema()` ### Testing diff --git a/src/hooks/pricing/README.md b/src/hooks/pricing/README.md index 23825a9..5bf171d 100644 --- a/src/hooks/pricing/README.md +++ b/src/hooks/pricing/README.md @@ -5,7 +5,7 @@ Calculate dynamic prices for products on Slice. Pricing strategies implement the ## How Pricing Works The `productPrice` function is called before purchase to determine: -- **ETH price** - Price in native currency +- **ETH price** - Price in ETH - **Currency price** - Price in ERC20 tokens (if applicable) ## Creating Custom Pricing @@ -29,9 +29,15 @@ The script will create your contract file with the proper template and add it to ### Registry Integration -Strategies inheriting from `RegistryProductPrice` automatically support frontend integration through: -- **Product configuration** via `configureProduct()` -- **Parameter validation** via `paramsSchema()` +Strategies can inherit from two base contracts: + +**`RegistryProductPrice`** - Product-level configuration: +- Configure via `configureProduct(slicerId, productId, variantId, params)` +- Schema via `configureProductSchema()` + +**`RegistryPrice`** - Multi-level configuration (Product, ProductType, or Slicer): +- Configure at product, product type, or slicer level +- Same hooks can be reused across different configuration scopes ### Testing @@ -40,4 +46,4 @@ The generator script will also create a test file for your pricing strategy. Cus ## Best Practices - Ensure the returned price adapts based on the quantity. -- Typically either ETH or currency price is returned, but you can return both if needed. \ No newline at end of file +- Typically either ETH or currency price is returned, but you can return both if needed. diff --git a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol index 26a586f..62affe8 100644 --- a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol @@ -3,7 +3,8 @@ pragma solidity ^0.8.30; import {IERC721} from "@openzeppelin-4.8.0/token/ERC721/IERC721.sol"; import {IERC1155} from "@openzeppelin-4.8.0/token/ERC1155/IERC1155.sol"; -import {HookRegistry, IHookRegistry, IProductsModule} from "@/utils/RegistryProductPrice.sol"; +import {HookRegistry, IProductsModule} from "@/utils/RegistryPrice.sol"; +import {ProductHookRegistry, IProductHookRegistry} from "@/utils/RegistryProductPrice.sol"; import {DiscountParams, TieredDiscount} from "../TieredDiscount.sol"; import {NFTType} from "../types/DiscountParams.sol"; @@ -24,16 +25,51 @@ contract NFTDiscount is TieredDiscount { //////////////////////////////////////////////////////////////*/ /** - * @inheritdoc HookRegistry + * @inheritdoc ProductHookRegistry * @notice Set base price and NFT discounts for a product. * @dev Discounts must be sorted in descending order and expressed as a percentage of the base price as a 4 decimal fixed point number. */ - function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { - (DiscountParams[] memory newDiscounts) = abi.decode(params, (DiscountParams[])); + function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) + internal + override + { + delete discounts[slicerId][productId][variantId]; + handleConfigure(discounts[slicerId][productId][variantId], params); + } + + /** + * @inheritdoc HookRegistry + * @notice Set base price and NFT discounts for a product type. + * @dev Discounts must be sorted in descending order and expressed as a percentage of the base price as a 4 decimal fixed point number. + */ + function _configureProductType(uint256 slicerId, uint256 productTypeId, bytes memory params) internal override { + delete productTypeDiscounts[slicerId][productTypeId]; + handleConfigure(productTypeDiscounts[slicerId][productTypeId], params); + } + + /** + * @inheritdoc HookRegistry + * @notice Set base price and NFT discounts for a slicer. + * @dev Discounts must be sorted in descending order and expressed as a percentage of the base price as a 4 decimal fixed point number. + */ + function _configureSlicer(uint256 slicerId, bytes memory params) internal override { + delete slicerDiscounts[slicerId]; + handleConfigure(slicerDiscounts[slicerId], params); + } + + /** + * @inheritdoc IProductHookRegistry + */ + function configureProductSchema() public pure override returns (string memory) { + return "(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[] discounts"; + } - DiscountParams[] storage productDiscount = discounts[slicerId][productId]; + /*////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////*/ - delete discounts[slicerId][productId]; + function handleConfigure(DiscountParams[] storage storageDiscounts, bytes memory params) internal { + (DiscountParams[] memory newDiscounts) = abi.decode(params, (DiscountParams[])); uint256 prevDiscountValue; DiscountParams memory discountParam; @@ -57,7 +93,7 @@ contract NFTDiscount is TieredDiscount { } prevDiscountValue = discountParam.discount; - productDiscount.push(discountParam); + storageDiscounts.push(discountParam); unchecked { ++i; @@ -65,42 +101,22 @@ contract NFTDiscount is TieredDiscount { } } - /** - * @inheritdoc IHookRegistry - */ - function paramsSchema() external pure override returns (string memory) { - return "(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[] discounts"; - } - /** * @inheritdoc TieredDiscount * @notice Base price is returned if user does not have a discount. */ - function _productPrice( - uint256, - uint256, - address currency, - uint256 quantity, - address buyer, - bytes memory, - uint256 basePrice, - DiscountParams[] memory discountParams - ) internal view virtual override returns (uint256 ethPrice, uint256 currencyPrice) { + function _productPrice(address buyer, uint256 basePriceAmount, DiscountParams[] memory discountParams) + internal + view + virtual + override + returns (uint256 price) + { uint256 discount = _getHighestDiscount(discountParams, buyer); - uint256 price = discount != 0 ? _getDiscountedPrice(basePrice, discount, quantity) : quantity * basePrice; - - if (currency == address(0)) { - ethPrice = price; - } else { - currencyPrice = price; - } + price = discount != 0 ? _getDiscountedPrice(basePriceAmount, discount) : basePriceAmount; } - /*////////////////////////////////////////////////////////////// - INTERNAL - //////////////////////////////////////////////////////////////*/ - /** * @notice Gets the highest discount available for a user, based on owned NFTs. * @@ -155,13 +171,12 @@ contract NFTDiscount is TieredDiscount { /** * @notice Calculate price based on `discountType` * - * @param basePrice Base price of the product + * @param basePriceAmount Base price of the product * @param discount Discount value based on `discountType` - * @param quantity Number of units purchased * * @return price of product inclusive of discount. */ - function _getDiscountedPrice(uint256 basePrice, uint256 discount, uint256 quantity) + function _getDiscountedPrice(uint256 basePriceAmount, uint256 discount) internal pure virtual @@ -173,6 +188,6 @@ contract NFTDiscount is TieredDiscount { k = 1e4 - discount; } - price = (basePrice * k * quantity) / 1e4; + price = (basePriceAmount * k) / 1e4; } } diff --git a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol index 0d85557..8f95990 100644 --- a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol @@ -2,7 +2,13 @@ pragma solidity ^0.8.30; import {IProductsModule} from "slice/interfaces/IProductsModule.sol"; -import {RegistryProductPrice, IProductPrice} from "@/utils/RegistryProductPrice.sol"; +import { + RegistryPrice, + IProductPrice, + IProductTypePrice, + ISlicerProductPrice, + ProductLineItem +} from "@/utils/RegistryPrice.sol"; import {DiscountParams} from "./types/DiscountParams.sol"; /** @@ -10,7 +16,7 @@ import {DiscountParams} from "./types/DiscountParams.sol"; * @notice Tiered discounts based on asset ownership * @author Slice */ -abstract contract TieredDiscount is RegistryProductPrice { +abstract contract TieredDiscount is RegistryPrice { /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ @@ -24,13 +30,16 @@ abstract contract TieredDiscount is RegistryProductPrice { MUTABLE STORAGE //////////////////////////////////////////////////////////////*/ - mapping(uint256 slicerId => mapping(uint256 productId => DiscountParams[])) public discounts; + mapping(uint256 slicerId => mapping(uint256 productId => mapping(uint256 variantId => DiscountParams[]))) public + discounts; + mapping(uint256 slicerId => mapping(uint256 productTypeId => DiscountParams[])) public productTypeDiscounts; + mapping(uint256 slicerId => DiscountParams[]) public slicerDiscounts; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(IProductsModule productsModuleAddress) RegistryProductPrice(productsModuleAddress) {} + constructor(IProductsModule productsModuleAddress) RegistryPrice(productsModuleAddress) {} /*////////////////////////////////////////////////////////////// FUNCTIONS @@ -42,18 +51,53 @@ abstract contract TieredDiscount is RegistryProductPrice { function productPrice( uint256 slicerId, uint256 productId, - address currency, - uint256 quantity, + uint256 variantId, + address, + uint256 basePrice, + uint256, address buyer, - bytes memory data - ) public view override returns (uint256 ethPrice, uint256 currencyPrice) { - (uint256 basePriceEth, uint256 basePriceCurrency) = - PRODUCTS_MODULE.basePrice(slicerId, productId, currency, quantity); - uint256 basePrice = currency == address(0) ? basePriceEth : basePriceCurrency; + bytes memory + ) public view override returns (uint256) { + DiscountParams[] memory discountParams = discounts[slicerId][productId][variantId]; + + return _productPrice(buyer, basePrice, discountParams); + } + + /** + * @inheritdoc IProductTypePrice + */ + function productTypePrice( + uint256 slicerId, + uint256 productTypeId, + address, + uint256 basePrice, + uint256, + address buyer + ) public view override returns (uint256) { + DiscountParams[] memory discountParams = productTypeDiscounts[slicerId][productTypeId]; + + return _productPrice(buyer, basePrice, discountParams); + } - DiscountParams[] memory discountParams = discounts[slicerId][productId]; + /** + * @inheritdoc ISlicerProductPrice + */ + function slicerProductPrice( + uint256 slicerId, + address, + address buyer, + ProductLineItem[] memory productLineItems, + uint256[] memory basePrices + ) public view override returns (uint256[] memory updatedBasePrices) { + DiscountParams[] memory discountParams = slicerDiscounts[slicerId]; + updatedBasePrices = new uint256[](basePrices.length); - return _productPrice(slicerId, productId, currency, quantity, buyer, data, basePrice, discountParams); + for (uint256 i; i < productLineItems.length;) { + updatedBasePrices[i] = _productPrice(buyer, basePrices[i], discountParams); + unchecked { + ++i; + } + } } /*////////////////////////////////////////////////////////////// @@ -63,25 +107,15 @@ abstract contract TieredDiscount is RegistryProductPrice { /** * @notice Logic for calculating product price. To be implemented by child contracts. * - * @param slicerId ID of the slicer to set the price params for. - * @param productId ID of the product to set the price params for. - * @param currency Currency chosen for the purchase - * @param quantity Number of units purchased * @param buyer Address of the buyer. - * @param data Data passed to the productPrice function. - * @param basePrice Base price of the product. + * @param basePrice Base price of the product in selected currency. * @param discountParams Array of discount parameters. * - * @return ethPrice and currencyPrice of product. + * @return Price of product in selected currency. */ - function _productPrice( - uint256 slicerId, - uint256 productId, - address currency, - uint256 quantity, - address buyer, - bytes memory data, - uint256 basePrice, - DiscountParams[] memory discountParams - ) internal view virtual returns (uint256 ethPrice, uint256 currencyPrice); + function _productPrice(address buyer, uint256 basePrice, DiscountParams[] memory discountParams) + internal + view + virtual + returns (uint256); } diff --git a/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol index ab0ad08..96d7b42 100644 --- a/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol @@ -1,10 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import {HookRegistry, IProductPrice, IHookRegistry, IProductsModule} from "@/utils/RegistryProductPrice.sol"; +import { + ProductHookRegistry, + IProductPrice, + IProductHookRegistry, + IProductsModule +} from "@/utils/RegistryProductPrice.sol"; import {wadLn, unsafeWadDiv, toDaysWadUnsafe} from "@/utils/math/SignedWadMath.sol"; -import {LinearProductParams} from "../types/LinearProductParams.sol"; -import {LinearVRGDAParams} from "../types/LinearVRGDAParams.sol"; +import {LinearProduct} from "../types/LinearProduct.sol"; import {VRGDAPrices} from "../VRGDAPrices.sol"; /// @title LinearVRGDAPrices @@ -16,7 +20,8 @@ contract LinearVRGDAPrices is VRGDAPrices { //////////////////////////////////////////////////////////////*/ // Mapping from slicerId to productId to ProductParams - mapping(uint256 => mapping(uint256 => LinearProductParams)) private _productParams; + mapping(uint256 slicerId => mapping(uint256 productId => mapping(uint256 variantId => LinearProduct))) private + _productParams; /*////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -29,12 +34,14 @@ contract LinearVRGDAPrices is VRGDAPrices { //////////////////////////////////////////////////////////////*/ /** - * @inheritdoc HookRegistry - * @notice Set LinearVRGDAParams for a product. + * @inheritdoc ProductHookRegistry + * @notice Set LinearProduct for a product. */ - function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { - (LinearVRGDAParams[] memory linearParams, int256 priceDecayPercent) = - abi.decode(params, (LinearVRGDAParams[], int256)); + function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) + internal + override + { + (int184 priceDecayPercent, uint16 min, int256 perTimeUnit) = abi.decode(params, (int184, uint16, int256)); int256 decayConstant = wadLn(1e18 - priceDecayPercent); // The decay constant must be negative for VRGDAs to work. @@ -42,25 +49,19 @@ contract LinearVRGDAPrices is VRGDAPrices { require(decayConstant >= type(int184).min, "MIN_DECAY_CONSTANT_EXCEEDED"); /// Get product availability and isInfinite - /// @dev available units is a uint32 - (uint256 availableUnits, bool isInfinite) = PRODUCTS_MODULE.availableUnits(slicerId, productId); + (uint32 stockUnits_, bool isInfinite) = getStockUnits(slicerId, productId, 0); // Product must not have infinite availability require(!isInfinite, "NOT_FINITE_AVAILABILITY"); // Set product params - _productParams[slicerId][productId].startTime = uint40(block.timestamp); - _productParams[slicerId][productId].startUnits = uint32(availableUnits); - _productParams[slicerId][productId].decayConstant = int184(decayConstant); - - // Set currency params - for (uint256 i; i < linearParams.length;) { - _productParams[slicerId][productId].pricingParams[linearParams[i].currency] = linearParams[i]; - - unchecked { - ++i; - } - } + _productParams[slicerId][productId][variantId] = LinearProduct({ + startTime: uint40(block.timestamp), + startUnits: stockUnits_, + decayConstant: int184(decayConstant), // forge-lint: disable-line(unsafe-typecast) + min: min, + perTimeUnit: perTimeUnit + }); } /** @@ -69,53 +70,42 @@ contract LinearVRGDAPrices is VRGDAPrices { function productPrice( uint256 slicerId, uint256 productId, + uint256 variantId, address currency, + uint256, uint256 quantity, address, bytes memory - ) public view override returns (uint256 ethPrice, uint256 currencyPrice) { - // Add reference for product and pricing params - LinearProductParams storage productParams = _productParams[slicerId][productId]; - LinearVRGDAParams memory pricingParams = productParams.pricingParams[currency]; + ) public view override returns (uint256 price) { + LinearProduct storage productParams = _productParams[slicerId][productId][variantId]; require(productParams.startTime != 0, "PRODUCT_UNSET"); - // Get available units - (uint256 availableUnits,) = PRODUCTS_MODULE.availableUnits(slicerId, productId); - - // Calculate sold units from availableUnits - uint256 soldUnits = productParams.startUnits - availableUnits; - - // Set ethPrice or currencyPrice based on chosen currency - if (currency == address(0)) { - ethPrice = getAdjustedVRGDAPrice( - pricingParams.targetPrice, - productParams.decayConstant, - toDaysWadUnsafe(block.timestamp - productParams.startTime), - soldUnits, - pricingParams.perTimeUnit, - pricingParams.min, - quantity - ); - } else { - currencyPrice = getAdjustedVRGDAPrice( - pricingParams.targetPrice, - productParams.decayConstant, - toDaysWadUnsafe(block.timestamp - productParams.startTime), - soldUnits, - pricingParams.perTimeUnit, - pricingParams.min, - quantity - ); - } + // Get available units and base price + (uint256 stockUnits_,) = getStockUnits(slicerId, productId, variantId); + uint256 basePrice_ = getBasePrice(slicerId, productId, variantId, currency, 1); + + if (basePrice_ > uint256(type(int256).max)) revert("BASE_PRICE_TOO_HIGH"); + + // Calculate sold units from stockUnits + uint256 soldUnits = productParams.startUnits - stockUnits_; + + price = getAdjustedVRGDAPrice( + int256(basePrice_), // forge-lint: disable-line(unsafe-typecast) + productParams.decayConstant, + toDaysWadUnsafe(block.timestamp - productParams.startTime), + soldUnits, + productParams.perTimeUnit, + basePrice_ * productParams.min / 1e5, + quantity + ); } /** - * @inheritdoc IHookRegistry + * @inheritdoc IProductHookRegistry */ - function paramsSchema() external pure override returns (string memory) { - return - "(address currency,int128 targetPrice,uint128 min,int256 perTimeUnit)[] linearParams,int256 priceDecayPercent"; + function configureProductSchema() public pure override returns (string memory) { + return "int184 priceDecayPercent,uint16 min,int256 perTimeUnit"; } /*////////////////////////////////////////////////////////////// diff --git a/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol b/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol index 5a0a114..8d70543 100644 --- a/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol @@ -1,7 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import {HookRegistry, IProductPrice, IHookRegistry, IProductsModule} from "@/utils/RegistryProductPrice.sol"; +import { + ProductHookRegistry, + IProductPrice, + IProductHookRegistry, + IProductsModule +} from "@/utils/RegistryProductPrice.sol"; import { wadMul, toWadUnsafe, @@ -12,8 +17,7 @@ import { wadExp, unsafeWadMul } from "@/utils/math/SignedWadMath.sol"; -import {LogisticProductParams} from "../types/LogisticProductParams.sol"; -import {LogisticVRGDAParams} from "../types/LogisticVRGDAParams.sol"; +import {LogisticProduct} from "../types/LogisticProduct.sol"; import {IProductsModule, VRGDAPrices} from "../VRGDAPrices.sol"; /// @title LogisticVRGDAPrices @@ -24,8 +28,9 @@ contract LogisticVRGDAPrices is VRGDAPrices { STORAGE //////////////////////////////////////////////////////////////*/ - // Mapping from slicerId to productId to LogisticProductParams - mapping(uint256 => mapping(uint256 => LogisticProductParams)) private _productParams; + // Mapping from slicerId to productId to LogisticProduct + mapping(uint256 slicerId => mapping(uint256 productId => mapping(uint256 variantId => LogisticProduct))) private + _productParams; /*////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -38,12 +43,14 @@ contract LogisticVRGDAPrices is VRGDAPrices { //////////////////////////////////////////////////////////////*/ /** - * @inheritdoc HookRegistry + * @inheritdoc ProductHookRegistry * @notice Set LogisticVRGDAParams for a product. */ - function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { - (LogisticVRGDAParams[] memory logisticParams, int256 priceDecayPercent) = - abi.decode(params, (LogisticVRGDAParams[], int256)); + function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) + internal + override + { + (int184 priceDecayPercent, uint16 min, int256 timescale) = abi.decode(params, (int184, uint16, int256)); int256 decayConstant = wadLn(1e18 - priceDecayPercent); // The decay constant must be negative for VRGDAs to work. @@ -51,24 +58,19 @@ contract LogisticVRGDAPrices is VRGDAPrices { require(decayConstant >= type(int184).min, "MIN_DECAY_CONSTANT_EXCEEDED"); // Get product availability and isInfinite - (uint256 availableUnits, bool isInfinite) = PRODUCTS_MODULE.availableUnits(slicerId, productId); + (uint32 stockUnits_, bool isInfinite) = getStockUnits(slicerId, productId, 0); // Product must not have infinite availability require(!isInfinite, "NON_FINITE_AVAILABILITY"); // Set product params - _productParams[slicerId][productId].startTime = uint40(block.timestamp); - _productParams[slicerId][productId].startUnits = uint32(availableUnits); - _productParams[slicerId][productId].decayConstant = int184(decayConstant); - - // Set currency params - for (uint256 i; i < logisticParams.length;) { - _productParams[slicerId][productId].pricingParams[logisticParams[i].currency] = logisticParams[i]; - - unchecked { - ++i; - } - } + _productParams[slicerId][productId][variantId] = LogisticProduct({ + startTime: uint40(block.timestamp), + startUnits: stockUnits_, + decayConstant: int184(decayConstant), // forge-lint: disable-line(unsafe-typecast) + min: min, + timeScale: timescale + }); } /** @@ -77,52 +79,43 @@ contract LogisticVRGDAPrices is VRGDAPrices { function productPrice( uint256 slicerId, uint256 productId, + uint256 variantId, address currency, + uint256, uint256 quantity, address, bytes memory - ) public view override returns (uint256 ethPrice, uint256 currencyPrice) { - // Add reference for product and pricing params - LogisticProductParams storage productParams = _productParams[slicerId][productId]; - LogisticVRGDAParams memory pricingParams = productParams.pricingParams[currency]; + ) public view override returns (uint256 price) { + LogisticProduct storage productParams = _productParams[slicerId][productId][variantId]; require(productParams.startTime != 0, "PRODUCT_UNSET"); - // Get available units - (uint256 availableUnits,) = PRODUCTS_MODULE.availableUnits(slicerId, productId); - - // Set ethPrice or currencyPrice based on chosen currency - if (currency == address(0)) { - ethPrice = getAdjustedVRGDALogisticPrice( - pricingParams.targetPrice, - productParams.decayConstant, - toDaysWadUnsafe(block.timestamp - productParams.startTime), - toWadUnsafe(productParams.startUnits + 1), - productParams.startUnits - availableUnits, - pricingParams.timeScale, - pricingParams.min, - quantity - ); - } else { - currencyPrice = getAdjustedVRGDALogisticPrice( - pricingParams.targetPrice, - productParams.decayConstant, - toDaysWadUnsafe(block.timestamp - productParams.startTime), - toWadUnsafe(productParams.startUnits + 1), - productParams.startUnits - availableUnits, - pricingParams.timeScale, - pricingParams.min, - quantity - ); - } + // Get available units and base price + (uint256 stockUnits_,) = getStockUnits(slicerId, productId, variantId); + uint256 basePrice_ = getBasePrice(slicerId, productId, variantId, currency, 1); + + if (basePrice_ > uint256(type(int256).max)) revert("BASE_PRICE_TOO_HIGH"); + + // Calculate sold units from stockUnits + uint256 soldUnits = productParams.startUnits - stockUnits_; + + price = getAdjustedVRGDALogisticPrice( + int256(basePrice_), // forge-lint: disable-line(unsafe-typecast) + productParams.decayConstant, + toDaysWadUnsafe(block.timestamp - productParams.startTime), + toWadUnsafe(productParams.startUnits + 1), + soldUnits, + productParams.timeScale, + basePrice_ * productParams.min / 1e5, + quantity + ); } /** - * @inheritdoc IHookRegistry + * @inheritdoc IProductHookRegistry */ - function paramsSchema() external pure override returns (string memory) { - return - "(address currency,int128 targetPrice,uint128 min,int256 timeScale)[] logisticParams,int256 priceDecayPercent"; + function configureProductSchema() public pure override returns (string memory) { + return "int184 priceDecayPercent,uint16 min,int256 timescale"; } /*////////////////////////////////////////////////////////////// diff --git a/src/hooks/pricing/VRGDA/types/LogisticProductParams.sol b/src/hooks/pricing/VRGDA/types/LinearProduct.sol similarity index 57% rename from src/hooks/pricing/VRGDA/types/LogisticProductParams.sol rename to src/hooks/pricing/VRGDA/types/LinearProduct.sol index fecc2bb..924d755 100644 --- a/src/hooks/pricing/VRGDA/types/LogisticProductParams.sol +++ b/src/hooks/pricing/VRGDA/types/LinearProduct.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import {LogisticVRGDAParams} from "./LogisticVRGDAParams.sol"; - /// @param startTime Time when the VRGDA began. /// @param startUnits Units available at the time when product is set up. /// @param decayConstant Precomputed constant that allows us to rewrite a pow() as an exp(). -/// @param pricingParams See `LogisticVRGDAParams` -struct LogisticProductParams { +/// @param min minimum price to be paid for a token, in percentage of the target price (1e5 = 100%) +/// @param perTimeUnit The total number of products to target selling every full unit of time. +struct LinearProduct { uint40 startTime; uint32 startUnits; int184 decayConstant; - mapping(address => LogisticVRGDAParams) pricingParams; + uint16 min; + int256 perTimeUnit; } diff --git a/src/hooks/pricing/VRGDA/types/LinearVRGDAParams.sol b/src/hooks/pricing/VRGDA/types/LinearVRGDAParams.sol deleted file mode 100644 index 563c36b..0000000 --- a/src/hooks/pricing/VRGDA/types/LinearVRGDAParams.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.30; - -/// @param targetPrice Target price for a product, to be scaled according to sales pace. -/// @param min minimum price to be paid for a token, scaled by 1e18 -/// @param perTimeUnit The total number of products to target selling every full unit of time. -struct LinearVRGDAParams { - address currency; - int128 targetPrice; - uint128 min; - int256 perTimeUnit; -} diff --git a/src/hooks/pricing/VRGDA/types/LinearProductParams.sol b/src/hooks/pricing/VRGDA/types/LogisticProduct.sol similarity index 62% rename from src/hooks/pricing/VRGDA/types/LinearProductParams.sol rename to src/hooks/pricing/VRGDA/types/LogisticProduct.sol index 365e992..2e596d0 100644 --- a/src/hooks/pricing/VRGDA/types/LinearProductParams.sol +++ b/src/hooks/pricing/VRGDA/types/LogisticProduct.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import {LinearVRGDAParams} from "./LinearVRGDAParams.sol"; - /// @param startTime Time when the VRGDA began. /// @param startUnits Units available at the time when product is set up. /// @param decayConstant Precomputed constant that allows us to rewrite a pow() as an exp(). -/// @param pricingParams See `LinearVRGDAParams` -struct LinearProductParams { +/// @param min minimum price to be paid for a token, scaled by 1e18 +/// @param timeScale Time scale controls the steepness of the logistic curve, +struct LogisticProduct { uint40 startTime; uint32 startUnits; int184 decayConstant; - mapping(address => LinearVRGDAParams) pricingParams; + uint128 min; + int256 timeScale; } diff --git a/src/hooks/pricing/VRGDA/types/LogisticVRGDAParams.sol b/src/hooks/pricing/VRGDA/types/LogisticVRGDAParams.sol deleted file mode 100644 index 15da7a1..0000000 --- a/src/hooks/pricing/VRGDA/types/LogisticVRGDAParams.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.30; - -/// @param targetPrice Target price for a product, to be scaled according to sales pace. -/// @param min minimum price to be paid for a token, scaled by 1e18 -/// @param timeScale Time scale controls the steepness of the logistic curve, -/// which affects how quickly we will reach the curve's asymptote. -struct LogisticVRGDAParams { - address currency; - int128 targetPrice; - uint128 min; - int256 timeScale; -} diff --git a/src/hooks/pricingActions/FirstForFree/FirstForFree.sol b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol index ff2f870..6bda187 100644 --- a/src/hooks/pricingActions/FirstForFree/FirstForFree.sol +++ b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol @@ -7,10 +7,10 @@ import { IProductPrice, RegistryProductAction, RegistryProductPriceAction, - IHookRegistry, + IProductHookRegistry, IProductsModule } from "@/utils/RegistryProductPriceAction.sol"; -import {HookRegistry} from "@/utils/RegistryProductAction.sol"; +import {ProductHookRegistry} from "@/utils/RegistryProductAction.sol"; import {ProductParams, TokenCondition} from "./types/ProductParams.sol"; import {TokenType} from "./types/TokenCondition.sol"; import {ITokenERC1155} from "./utils/ITokenERC1155.sol"; @@ -25,8 +25,8 @@ contract FirstForFree is RegistryProductPriceAction { MUTABLE STORAGE //////////////////////////////////////////////////////////////*/ - mapping(uint256 slicerId => mapping(uint256 productId => ProductParams price)) public usdcPrices; - mapping(address buyer => mapping(uint256 slicerId => uint256 purchases)) public purchases; + mapping(uint256 slicerId => mapping(uint256 productId => mapping(uint256 variantId => ProductParams price))) public + productParams; /*////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -42,76 +42,76 @@ contract FirstForFree is RegistryProductPriceAction { * @inheritdoc IProductPrice * @notice Applies discount only for first N purchases on a slicer. */ - function productPrice(uint256 slicerId, uint256 productId, address, uint256 quantity, address buyer, bytes memory) - public - view - override - returns (uint256 ethPrice, uint256 currencyPrice) - { - ProductParams memory productParams = usdcPrices[slicerId][productId]; + function productPrice( + uint256 slicerId, + uint256 productId, + uint256 variantId, + address, + uint256 basePrice, + uint256 quantity, + address buyer, + bytes memory + ) public view override returns (uint256) { + ProductParams memory params = productParams[slicerId][productId][variantId]; - if (_isEligible(buyer, productParams.eligibleTokens)) { - uint256 totalPurchases = purchases[buyer][slicerId]; - if (totalPurchases < productParams.freeUnits) { + if (_isEligible(buyer, params.eligibleTokens)) { + uint256 totalPurchases = getPurchases(buyer, slicerId, productId, variantId); + if (totalPurchases < params.freeUnits) { unchecked { - uint256 freeUnitsLeft = productParams.freeUnits - totalPurchases; + uint256 freeUnitsLeft = params.freeUnits - totalPurchases; + if (quantity <= freeUnitsLeft) { - return (0, 0); + return 0; } else { - return (0, usdcPrices[slicerId][productId].usdcPrice * (quantity - freeUnitsLeft)); + return getBasePrice(slicerId, productId, variantId, address(0), quantity - freeUnitsLeft); } } } } - return (0, usdcPrices[slicerId][productId].usdcPrice * quantity); + return basePrice; } /** * @inheritdoc RegistryProductAction - * @notice Mint `quantity` NFTs to `account` on purchase. Keeps track of total purchases. + * @notice Mint `quantity` NFTs to `account` on purchase. */ function _onProductPurchase( uint256 slicerId, uint256 productId, + uint256 variantId, address buyer, uint256 quantity, - bytes memory, bytes memory ) internal override { - purchases[buyer][slicerId] += quantity; - - ProductParams memory productParams = usdcPrices[slicerId][productId]; - if (productParams.mintToken != address(0)) { - ITokenERC1155(productParams.mintToken).mintTo(buyer, productParams.mintTokenId, "", quantity); + ProductParams memory params = productParams[slicerId][productId][variantId]; + if (params.mintToken != address(0)) { + ITokenERC1155(params.mintToken).mintTo(buyer, params.mintTokenId, "", quantity); } } /** - * @inheritdoc HookRegistry + * @inheritdoc ProductHookRegistry * @notice Sets the product parameters. */ - function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { - ( - uint256 usdcPrice, - TokenCondition[] memory eligibleTokens, - address mintToken, - uint88 mintTokenId, - uint8 freeUnits - ) = abi.decode(params, (uint256, TokenCondition[], address, uint88, uint8)); - - ProductParams storage productParams = usdcPrices[slicerId][productId]; - - productParams.usdcPrice = usdcPrice; - productParams.mintToken = mintToken; - productParams.mintTokenId = mintTokenId; - productParams.freeUnits = freeUnits; + function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) + internal + override + { + (TokenCondition[] memory eligibleTokens, address mintToken, uint88 mintTokenId, uint8 freeUnits) = + abi.decode(params, (TokenCondition[], address, uint88, uint8)); + + ProductParams storage productParams_ = productParams[slicerId][productId][variantId]; + + productParams_.mintToken = mintToken; + productParams_.mintTokenId = mintTokenId; + productParams_.freeUnits = freeUnits; // Remove all discount tokens - delete productParams.eligibleTokens; + delete productParams_.eligibleTokens; for (uint256 i = 0; i < eligibleTokens.length;) { - productParams.eligibleTokens.push(eligibleTokens[i]); + productParams_.eligibleTokens.push(eligibleTokens[i]); unchecked { ++i; @@ -120,11 +120,11 @@ contract FirstForFree is RegistryProductPriceAction { } /** - * @inheritdoc IHookRegistry + * @inheritdoc IProductHookRegistry */ - function paramsSchema() external pure override returns (string memory) { + function configureProductSchema() public pure override returns (string memory) { return - "uint256 usdcPrice,(address tokenAddress,uint8 tokenType,uint88 tokenId,uint8 minQuantity)[] eligibleTokens,address mintToken,uint88 mintTokenId,uint8 freeUnits"; + "(address tokenAddress,uint8 tokenType,uint80 tokenId,uint8 minQuantity)[] eligibleTokens,address mintToken,uint88 mintTokenId,uint8 freeUnits"; } /*////////////////////////////////////////////////////////////// diff --git a/src/hooks/pricingActions/FirstForFree/types/ProductParams.sol b/src/hooks/pricingActions/FirstForFree/types/ProductParams.sol index 2e7c964..8066db9 100644 --- a/src/hooks/pricingActions/FirstForFree/types/ProductParams.sol +++ b/src/hooks/pricingActions/FirstForFree/types/ProductParams.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.30; import {TokenCondition} from "./TokenCondition.sol"; struct ProductParams { - uint256 usdcPrice; TokenCondition[] eligibleTokens; address mintToken; uint88 mintTokenId; diff --git a/src/hooks/pricingActions/README.md b/src/hooks/pricingActions/README.md index 80c5c86..d768adb 100644 --- a/src/hooks/pricingActions/README.md +++ b/src/hooks/pricingActions/README.md @@ -36,7 +36,7 @@ The script will create your contract file with the proper template and add it to Hooks inheriting from `RegistryProductPriceAction` automatically support frontend integration through: - **Product configuration** via `configureProduct()` -- **Parameter validation** via `paramsSchema()` +- **Parameter validation** via `configureProductSchema()` ### Testing diff --git a/src/interfaces/IHookRegistry.sol b/src/interfaces/IHookRegistry.sol index 1b04144..7019425 100644 --- a/src/interfaces/IHookRegistry.sol +++ b/src/interfaces/IHookRegistry.sol @@ -1,4 +1,4 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import "slice/interfaces/hooks/IHookRegistry.sol"; +import {IProductHookRegistry, IProductTypeHookRegistry, ISlicerHookRegistry} from "slice/interfaces.sol"; diff --git a/src/interfaces/IProductAction.sol b/src/interfaces/IProductAction.sol index 1fd0423..fd6fbf9 100644 --- a/src/interfaces/IProductAction.sol +++ b/src/interfaces/IProductAction.sol @@ -1,4 +1,4 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import "slice/interfaces/hooks/IProductAction.sol"; +import {IProductAction} from "slice/interfaces.sol"; diff --git a/src/interfaces/IProductPrice.sol b/src/interfaces/IProductPrice.sol index fb9c752..6d44d16 100644 --- a/src/interfaces/IProductPrice.sol +++ b/src/interfaces/IProductPrice.sol @@ -1,4 +1,4 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import "slice/interfaces/hooks/IProductPrice.sol"; +import {IProductPrice, IProductTypePrice, ISlicerProductPrice} from "slice/interfaces.sol"; diff --git a/src/utils/RegistryAction.sol b/src/utils/RegistryAction.sol new file mode 100644 index 0000000..1c95fa8 --- /dev/null +++ b/src/utils/RegistryAction.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +import "slice/utils/hooks/RegistryAction.sol"; diff --git a/src/utils/RegistryPrice.sol b/src/utils/RegistryPrice.sol new file mode 100644 index 0000000..56a727c --- /dev/null +++ b/src/utils/RegistryPrice.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +import "slice/utils/hooks/RegistryPrice.sol"; diff --git a/test/actions/Allowlisted/Allowlisted.t.sol b/test/actions/Allowlisted/Allowlisted.t.sol index a59108f..c95edab 100644 --- a/test/actions/Allowlisted/Allowlisted.t.sol +++ b/test/actions/Allowlisted/Allowlisted.t.sol @@ -7,6 +7,8 @@ import {Allowlisted} from "@/hooks/actions/Allowlisted/Allowlisted.sol"; uint256 constant slicerId = 0; uint256 constant productId = 1; +uint256 constant variantId = 0; +uint256 constant variantIdAlt = 1; contract AllowlistedTest is RegistryProductActionTest { Allowlisted allowlisted; @@ -29,28 +31,63 @@ contract AllowlistedTest is RegistryProductActionTest { bytes32 root = m.getRoot(data); vm.prank(productOwner); - allowlisted.configureProduct(slicerId, productId, abi.encode(root)); + allowlisted.configureProduct(slicerId, productId, variantId, abi.encode(root)); - assertTrue(allowlisted.merkleRoots(slicerId, productId) == root); + assertTrue(allowlisted.merkleRoots(slicerId, productId, variantId) == root); } function testIsPurchaseAllowed() public { bytes32 root = m.getRoot(data); vm.prank(productOwner); - allowlisted.configureProduct(slicerId, productId, abi.encode(root)); + allowlisted.configureProduct(slicerId, productId, variantId, abi.encode(root)); bytes32[] memory proof = m.getProof(data, 0); - assertTrue(allowlisted.isPurchaseAllowed(slicerId, productId, buyer, 0, "", abi.encode(proof))); + assertTrue(allowlisted.isPurchaseAllowed(slicerId, productId, variantId, buyer, 0, abi.encode(proof))); } function testIsPurchaseAllowed_wrongProof() public { bytes32 root = m.getRoot(data); vm.prank(productOwner); - allowlisted.configureProduct(slicerId, productId, abi.encode(root)); + allowlisted.configureProduct(slicerId, productId, variantId, abi.encode(root)); bytes32[] memory wrongProof = m.getProof(data, 1); - assertFalse(allowlisted.isPurchaseAllowed(slicerId, productId, buyer, 0, "", abi.encode(wrongProof))); + assertFalse(allowlisted.isPurchaseAllowed(slicerId, productId, variantId, buyer, 0, abi.encode(wrongProof))); + } + + function testIsPurchaseAllowed_variantSpecificConfiguration() public { + bytes32 root = m.getRoot(data); + + vm.prank(productOwner); + allowlisted.configureProduct(slicerId, productId, variantId, abi.encode(root)); + + address variantBuyer = address(0xBEEF); + bytes32[] memory variantLeaves = new bytes32[](4); + variantLeaves[0] = bytes32(keccak256(abi.encode(variantBuyer))); + variantLeaves[1] = bytes32(keccak256(abi.encode(address(0xCAFE)))); + variantLeaves[2] = bytes32(keccak256(abi.encode(address(0xD00D)))); + variantLeaves[3] = bytes32(keccak256(abi.encode(address(0xFEED)))); + bytes32 variantRoot = m.getRoot(variantLeaves); + + vm.prank(productOwner); + allowlisted.configureProduct(slicerId, productId, variantIdAlt, abi.encode(variantRoot)); + + bytes32[] memory productProof = m.getProof(data, 0); + assertTrue(allowlisted.isPurchaseAllowed(slicerId, productId, variantId, buyer, 0, abi.encode(productProof))); + + // Product-level allowlist should not apply to variant configuration + assertFalse( + allowlisted.isPurchaseAllowed(slicerId, productId, variantIdAlt, buyer, 0, abi.encode(productProof)) + ); + + bytes32[] memory variantLeavesForProof = new bytes32[](variantLeaves.length); + for (uint256 i; i < variantLeaves.length; ++i) { + variantLeavesForProof[i] = variantLeaves[i]; + } + bytes32[] memory variantProof = m.getProof(variantLeavesForProof, 0); + assertTrue( + allowlisted.isPurchaseAllowed(slicerId, productId, variantIdAlt, variantBuyer, 0, abi.encode(variantProof)) + ); } } diff --git a/test/actions/ERC20Gated/ERC20Gated.t.sol b/test/actions/ERC20Gated/ERC20Gated.t.sol index a4d8ea3..92ba68f 100644 --- a/test/actions/ERC20Gated/ERC20Gated.t.sol +++ b/test/actions/ERC20Gated/ERC20Gated.t.sol @@ -5,9 +5,14 @@ import {RegistryProductActionTest} from "@test/utils/RegistryProductActionTest.s import {MockERC20Gated} from "./mocks/MockERC20Gated.sol"; import {ERC20Gate} from "@/hooks/actions/ERC20Gated/ERC20Gated.sol"; import {IERC20, MockERC20} from "@test/utils/mocks/MockERC20.sol"; +import {HookRegistry, ProductLineItem} from "@/utils/RegistryAction.sol"; +import {SliceContext} from "@/utils/RegistryProductAction.sol"; uint256 constant slicerId = 0; uint256 constant productId = 1; +uint256 constant variantId = 0; +uint256 constant variantIdAlt = 1; +uint256 constant productTypeId = 2; contract ERC20GatedTest is RegistryProductActionTest { MockERC20Gated erc20Gated; @@ -24,9 +29,9 @@ contract ERC20GatedTest is RegistryProductActionTest { gates[0] = ERC20Gate(token, 100); vm.prank(productOwner); - erc20Gated.configureProduct(slicerId, productId, abi.encode(gates)); + erc20Gated.configureProduct(slicerId, productId, variantId, abi.encode(gates)); - (IERC20 tokenAddr, uint256 amount) = erc20Gated.tokenGates(slicerId, productId, 0); + (IERC20 tokenAddr, uint256 amount) = erc20Gated.tokenGates(slicerId, productId, variantId, 0); assertTrue(address(tokenAddr) == address(token)); assertTrue(amount == 100); } @@ -37,22 +42,22 @@ contract ERC20GatedTest is RegistryProductActionTest { gates[1] = ERC20Gate(token2, 200); vm.startPrank(productOwner); - erc20Gated.configureProduct(slicerId, productId, abi.encode(gates)); + erc20Gated.configureProduct(slicerId, productId, variantId, abi.encode(gates)); - assertEq(address(erc20Gated.gates(slicerId, productId)[0].erc20), address(token)); - assertEq(erc20Gated.gates(slicerId, productId)[0].amount, 100); - assertEq(address(erc20Gated.gates(slicerId, productId)[1].erc20), address(token2)); - assertEq(erc20Gated.gates(slicerId, productId)[1].amount, 200); - assertEq(erc20Gated.gates(slicerId, productId).length, 2); + assertEq(address(erc20Gated.gates(slicerId, productId, variantId)[0].erc20), address(token)); + assertEq(erc20Gated.gates(slicerId, productId, variantId)[0].amount, 100); + assertEq(address(erc20Gated.gates(slicerId, productId, variantId)[1].erc20), address(token2)); + assertEq(erc20Gated.gates(slicerId, productId, variantId)[1].amount, 200); + assertEq(erc20Gated.gates(slicerId, productId, variantId).length, 2); MockERC20 token3 = new MockERC20("Test3", "TST3", 18); gates = new ERC20Gate[](1); gates[0] = ERC20Gate(token3, 300); - erc20Gated.configureProduct(slicerId, productId, abi.encode(gates)); - assertEq(address(erc20Gated.gates(slicerId, productId)[0].erc20), address(token3)); - assertEq(erc20Gated.gates(slicerId, productId)[0].amount, 300); - assertEq(erc20Gated.gates(slicerId, productId).length, 1); + erc20Gated.configureProduct(slicerId, productId, variantId, abi.encode(gates)); + assertEq(address(erc20Gated.gates(slicerId, productId, variantId)[0].erc20), address(token3)); + assertEq(erc20Gated.gates(slicerId, productId, variantId)[0].amount, 300); + assertEq(erc20Gated.gates(slicerId, productId, variantId).length, 1); vm.stopPrank(); } @@ -62,11 +67,273 @@ contract ERC20GatedTest is RegistryProductActionTest { gates[0] = ERC20Gate(token, 100); vm.prank(productOwner); - erc20Gated.configureProduct(slicerId, productId, abi.encode(gates)); + erc20Gated.configureProduct(slicerId, productId, variantId, abi.encode(gates)); - assertFalse(erc20Gated.isPurchaseAllowed(slicerId, productId, buyer, 0, "", "")); + assertFalse(erc20Gated.isPurchaseAllowed(slicerId, productId, variantId, buyer, 0, "")); token.mint(buyer, 100); - assertTrue(erc20Gated.isPurchaseAllowed(slicerId, productId, buyer, 0, "", "")); + assertTrue(erc20Gated.isPurchaseAllowed(slicerId, productId, variantId, buyer, 0, "")); + } + + function testIsPurchaseAllowed_variantSpecificConfiguration() public { + ERC20Gate[] memory productGates = new ERC20Gate[](1); + productGates[0] = ERC20Gate(token, 100); + + ERC20Gate[] memory variantGates = new ERC20Gate[](1); + variantGates[0] = ERC20Gate(token2, 200); + + vm.startPrank(productOwner); + erc20Gated.configureProduct(slicerId, productId, variantId, abi.encode(productGates)); + erc20Gated.configureProduct(slicerId, productId, variantIdAlt, abi.encode(variantGates)); + vm.stopPrank(); + + assertFalse(erc20Gated.isPurchaseAllowed(slicerId, productId, variantId, buyer, 0, "")); + assertFalse(erc20Gated.isPurchaseAllowed(slicerId, productId, variantIdAlt, buyer, 0, "")); + + token.mint(buyer, 100); + assertTrue(erc20Gated.isPurchaseAllowed(slicerId, productId, variantId, buyer, 0, "")); + // Variant gate still unmet as it requires a different token + assertFalse(erc20Gated.isPurchaseAllowed(slicerId, productId, variantIdAlt, buyer, 0, "")); + + token2.mint(buyer, 200); + assertTrue(erc20Gated.isPurchaseAllowed(slicerId, productId, variantIdAlt, buyer, 0, "")); + + (IERC20 tokenAddrDefault, uint256 amountDefault) = erc20Gated.tokenGates(slicerId, productId, variantId, 0); + (IERC20 tokenAddrVariant, uint256 amountVariant) = erc20Gated.tokenGates(slicerId, productId, variantIdAlt, 0); + + assertEq(address(tokenAddrDefault), address(token)); + assertEq(amountDefault, 100); + assertEq(address(tokenAddrVariant), address(token2)); + assertEq(amountVariant, 200); + } + + /*////////////////////////////////////////////////////////////// + PRODUCT TYPE TESTS + //////////////////////////////////////////////////////////////*/ + + function testConfigureProductType() public { + ERC20Gate[] memory gates = new ERC20Gate[](2); + gates[0] = ERC20Gate(token2, 200); + gates[1] = ERC20Gate(token, 100); + + bytes memory params = abi.encode(gates); + + vm.expectEmit(true, true, true, true, address(erc20Gated)); + emit HookRegistry.ProductTypeConfigured(slicerId, productTypeId, params); + + vm.prank(productOwner); + erc20Gated.configureProductType(slicerId, productTypeId, params); + + assertEq(address(erc20Gated.productTypeGates(slicerId, productTypeId)[0].erc20), address(token2)); + assertEq(erc20Gated.productTypeGates(slicerId, productTypeId)[0].amount, 200); + assertEq(address(erc20Gated.productTypeGates(slicerId, productTypeId)[1].erc20), address(token)); + assertEq(erc20Gated.productTypeGates(slicerId, productTypeId)[1].amount, 100); + assertEq(erc20Gated.productTypeGates(slicerId, productTypeId).length, 2); + + // Buyer doesn't have tokens yet + assertFalse(erc20Gated.isProductTypePurchaseAllowed(slicerId, productTypeId, buyer, 0)); + + // Buyer has token but not token2 + token.mint(buyer, 100); + assertFalse(erc20Gated.isProductTypePurchaseAllowed(slicerId, productTypeId, buyer, 0)); + + // Buyer has both tokens + token2.mint(buyer, 200); + assertTrue(erc20Gated.isProductTypePurchaseAllowed(slicerId, productTypeId, buyer, 0)); + } + + function testConfigureProductType_AccessControl() public { + vm.expectRevert(abi.encodeWithSelector(SliceContext.NotAuthorized.selector, bytes32(uint256(1 << 1 | 1 << 3)))); + + erc20Gated.configureProductType(slicerId, productTypeId, ""); + } + + function testConfigureProductTypeSchema() public view { + string memory schema = erc20Gated.configureProductTypeSchema(); + + assertEq(schema, erc20Gated.configureProductSchema()); + assertTrue(bytes(schema).length > 0); + } + + function testIsProductTypePurchaseAllowed_NoGates() public { + vm.prank(productOwner); + erc20Gated.configureProductType(slicerId, productTypeId, abi.encode(new ERC20Gate[](0))); + + assertTrue(erc20Gated.isProductTypePurchaseAllowed(slicerId, productTypeId, buyer2, 0)); + } + + function testConfigureProductType__Edit_Remove() public { + ERC20Gate[] memory gates = new ERC20Gate[](2); + gates[0] = ERC20Gate(token2, 200); + gates[1] = ERC20Gate(token, 100); + + vm.prank(productOwner); + erc20Gated.configureProductType(slicerId, productTypeId, abi.encode(gates)); + + token.mint(buyer, 100); + token2.mint(buyer, 200); + assertTrue(erc20Gated.isProductTypePurchaseAllowed(slicerId, productTypeId, buyer, 0)); + + // Remove gates + vm.prank(productOwner); + erc20Gated.configureProductType(slicerId, productTypeId, abi.encode(new ERC20Gate[](0))); + + assertTrue(erc20Gated.isProductTypePurchaseAllowed(slicerId, productTypeId, buyer, 0)); + assertEq(erc20Gated.productTypeGates(slicerId, productTypeId).length, 0); + } + + function testOnProductTypePurchase() public { + ERC20Gate[] memory gates = new ERC20Gate[](1); + gates[0] = ERC20Gate(token, 100); + + vm.prank(productOwner); + erc20Gated.configureProductType(slicerId, productTypeId, abi.encode(gates)); + + // Buyer doesn't have tokens - should revert + vm.expectRevert(); + vm.prank(address(PRODUCTS_MODULE)); + erc20Gated.onProductTypePurchase(slicerId, productTypeId, buyer, 1); + + // Buyer has tokens - should succeed + token.mint(buyer, 100); + vm.prank(address(PRODUCTS_MODULE)); + erc20Gated.onProductTypePurchase(slicerId, productTypeId, buyer, 1); + } + + function testRevert_OnProductTypePurchase_NotPurchase() public { + ERC20Gate[] memory gates = new ERC20Gate[](1); + gates[0] = ERC20Gate(token, 100); + + vm.prank(productOwner); + erc20Gated.configureProductType(slicerId, productTypeId, abi.encode(gates)); + + token.mint(buyer, 100); + + vm.expectRevert(); + erc20Gated.onProductTypePurchase(slicerId, productTypeId, buyer, 1); + } + + /*////////////////////////////////////////////////////////////// + SLICER TESTS + //////////////////////////////////////////////////////////////*/ + + function testConfigureSlicer() public { + ERC20Gate[] memory gates = new ERC20Gate[](2); + gates[0] = ERC20Gate(token2, 200); + gates[1] = ERC20Gate(token, 100); + + bytes memory params = abi.encode(gates); + + vm.expectEmit(true, true, true, true, address(erc20Gated)); + emit HookRegistry.SlicerConfigured(slicerId, params); + + vm.prank(productOwner); + erc20Gated.configureSlicer(slicerId, params); + + assertEq(address(erc20Gated.slicerGates(slicerId)[0].erc20), address(token2)); + assertEq(erc20Gated.slicerGates(slicerId)[0].amount, 200); + assertEq(address(erc20Gated.slicerGates(slicerId)[1].erc20), address(token)); + assertEq(erc20Gated.slicerGates(slicerId)[1].amount, 100); + assertEq(erc20Gated.slicerGates(slicerId).length, 2); + + // Buyer doesn't have tokens yet + ProductLineItem[] memory productLineItems = new ProductLineItem[](1); + productLineItems[0] = + ProductLineItem({productId: uint32(productId), variantId: uint32(variantId), quantity: uint32(5)}); + assertFalse(erc20Gated.isSlicerPurchaseAllowed(slicerId, buyer, productLineItems)); + + // Buyer has token but not token2 + token.mint(buyer, 100); + assertFalse(erc20Gated.isSlicerPurchaseAllowed(slicerId, buyer, productLineItems)); + + // Buyer has both tokens + token2.mint(buyer, 200); + assertTrue(erc20Gated.isSlicerPurchaseAllowed(slicerId, buyer, productLineItems)); + } + + function testConfigureSlicer_AccessControl() public { + vm.expectRevert(abi.encodeWithSelector(SliceContext.NotAuthorized.selector, bytes32(uint256(1 << 1 | 1 << 3)))); + + erc20Gated.configureSlicer(slicerId, ""); + } + + function testConfigureSlicerSchema() public view { + string memory schema = erc20Gated.configureSlicerSchema(); + + assertEq(schema, erc20Gated.configureProductSchema()); + assertTrue(bytes(schema).length > 0); + } + + function testIsSlicerPurchaseAllowed_NoGates() public { + vm.prank(productOwner); + erc20Gated.configureSlicer(slicerId, abi.encode(new ERC20Gate[](0))); + + ProductLineItem[] memory productLineItems = new ProductLineItem[](1); + productLineItems[0] = + ProductLineItem({productId: uint32(productId), variantId: uint32(variantId), quantity: uint32(5)}); + assertTrue(erc20Gated.isSlicerPurchaseAllowed(slicerId, buyer3, productLineItems)); + } + + function testConfigureSlicer__Edit_Remove() public { + ERC20Gate[] memory gates = new ERC20Gate[](2); + gates[0] = ERC20Gate(token2, 200); + gates[1] = ERC20Gate(token, 100); + + vm.prank(productOwner); + erc20Gated.configureSlicer(slicerId, abi.encode(gates)); + + token.mint(buyer, 100); + token2.mint(buyer, 200); + + ProductLineItem[] memory productLineItems = new ProductLineItem[](1); + productLineItems[0] = + ProductLineItem({productId: uint32(productId), variantId: uint32(variantId), quantity: uint32(5)}); + assertTrue(erc20Gated.isSlicerPurchaseAllowed(slicerId, buyer, productLineItems)); + + // Remove gates + vm.prank(productOwner); + erc20Gated.configureSlicer(slicerId, abi.encode(new ERC20Gate[](0))); + + assertTrue(erc20Gated.isSlicerPurchaseAllowed(slicerId, buyer, productLineItems)); + assertEq(erc20Gated.slicerGates(slicerId).length, 0); + } + + function testOnSlicerPurchase() public { + ERC20Gate[] memory gates = new ERC20Gate[](1); + gates[0] = ERC20Gate(token, 100); + + vm.prank(productOwner); + erc20Gated.configureSlicer(slicerId, abi.encode(gates)); + + ProductLineItem[] memory productLineItems = new ProductLineItem[](1); + productLineItems[0] = + ProductLineItem({productId: uint32(productId), variantId: uint32(variantId), quantity: uint32(5)}); + + // Buyer doesn't have tokens - should revert + vm.expectRevert(); + vm.prank(address(PRODUCTS_MODULE)); + erc20Gated.onSlicerPurchase(slicerId, buyer, productLineItems); + + // Buyer has tokens - should succeed + token.mint(buyer, 100); + vm.prank(address(PRODUCTS_MODULE)); + erc20Gated.onSlicerPurchase(slicerId, buyer, productLineItems); + } + + function testRevert_OnSlicerPurchase_NotPurchase() public { + ERC20Gate[] memory gates = new ERC20Gate[](1); + gates[0] = ERC20Gate(token, 100); + + vm.prank(productOwner); + erc20Gated.configureSlicer(slicerId, abi.encode(gates)); + + token.mint(buyer, 100); + + ProductLineItem[] memory productLineItems = new ProductLineItem[](1); + productLineItems[0] = + ProductLineItem({productId: uint32(productId), variantId: uint32(variantId), quantity: uint32(5)}); + + vm.expectRevert(); + erc20Gated.onSlicerPurchase(slicerId, buyer, productLineItems); } } diff --git a/test/actions/ERC20Gated/mocks/MockERC20Gated.sol b/test/actions/ERC20Gated/mocks/MockERC20Gated.sol index ea96650..f6eedc8 100644 --- a/test/actions/ERC20Gated/mocks/MockERC20Gated.sol +++ b/test/actions/ERC20Gated/mocks/MockERC20Gated.sol @@ -6,7 +6,15 @@ import {IProductsModule, ERC20Gated, ERC20Gate} from "@/hooks/actions/ERC20Gated contract MockERC20Gated is ERC20Gated { constructor(IProductsModule productsModuleAddress) ERC20Gated(productsModuleAddress) {} - function gates(uint256 slicerId, uint256 productId) public view returns (ERC20Gate[] memory) { - return tokenGates[slicerId][productId]; + function gates(uint256 slicerId, uint256 productId, uint256 variantId) public view returns (ERC20Gate[] memory) { + return tokenGates[slicerId][productId][variantId]; + } + + function productTypeGates(uint256 slicerId, uint256 productTypeId) public view returns (ERC20Gate[] memory) { + return productTypeTokenGates[slicerId][productTypeId]; + } + + function slicerGates(uint256 slicerId) public view returns (ERC20Gate[] memory) { + return slicerTokenGates[slicerId]; } } diff --git a/test/actions/ERC20Mint/ERC20Mint.t.sol b/test/actions/ERC20Mint/ERC20Mint.t.sol index 48921cc..80830f9 100644 --- a/test/actions/ERC20Mint/ERC20Mint.t.sol +++ b/test/actions/ERC20Mint/ERC20Mint.t.sol @@ -5,10 +5,13 @@ import {RegistryProductAction, RegistryProductActionTest} from "@test/utils/Regi import {ERC20Mint} from "@/hooks/actions/ERC20Mint/ERC20Mint.sol"; import {ERC20Data} from "@/hooks/actions/ERC20Mint/types/ERC20Data.sol"; import {ERC20Mint_BaseToken} from "@/hooks/actions/ERC20Mint/utils/ERC20Mint_BaseToken.sol"; - -import {console2} from "forge-std/console2.sol"; +import {HookRegistry, ProductLineItem} from "@/utils/RegistryAction.sol"; +import {SliceContext} from "@/utils/RegistryProductAction.sol"; uint256 constant slicerId = 0; +uint256 constant variantId = 0; +uint256 constant variantIdAlt = 1; +uint256 constant productTypeId = 2; contract ERC20MintTest is RegistryProductActionTest { ERC20Mint erc20Mint; @@ -27,6 +30,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test Token 1", // name "TT1", // symbol @@ -42,6 +46,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[1], + variantId, abi.encode( "Test Token 2", // name "TT2", // symbol @@ -57,6 +62,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[2], + variantId, abi.encode( "Test Token 3", // name "TT3", // symbol @@ -72,7 +78,7 @@ contract ERC20MintTest is RegistryProductActionTest { // Verify tokenData is set correctly (ERC20Mint_BaseToken token1, bool revertOnMaxSupply1, uint256 tokensPerUnit1) = - erc20Mint.tokenData(slicerId, productIds[0]); + erc20Mint.tokenData(slicerId, productIds[0], variantId); assertEq(revertOnMaxSupply1, true); assertEq(tokensPerUnit1, 100); assertEq(token1.name(), "Test Token 1"); @@ -82,7 +88,7 @@ contract ERC20MintTest is RegistryProductActionTest { assertEq(token1.balanceOf(productOwner), 1000); (ERC20Mint_BaseToken token2, bool revertOnMaxSupply2, uint256 tokensPerUnit2) = - erc20Mint.tokenData(slicerId, productIds[1]); + erc20Mint.tokenData(slicerId, productIds[1], variantId); assertEq(revertOnMaxSupply2, false); assertEq(tokensPerUnit2, 50); assertEq(token2.name(), "Test Token 2"); @@ -91,7 +97,7 @@ contract ERC20MintTest is RegistryProductActionTest { assertEq(token2.totalSupply(), 0); // no premint (ERC20Mint_BaseToken token3, bool revertOnMaxSupply3, uint256 tokensPerUnit3) = - erc20Mint.tokenData(slicerId, productIds[2]); + erc20Mint.tokenData(slicerId, productIds[2], variantId); assertEq(revertOnMaxSupply3, true); assertEq(tokensPerUnit3, 1); assertEq(token3.totalSupply(), 500); // premint amount @@ -105,6 +111,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test Token", // name "TT", // symbol @@ -116,13 +123,14 @@ contract ERC20MintTest is RegistryProductActionTest { ) ); - (ERC20Mint_BaseToken token1,,) = erc20Mint.tokenData(slicerId, productIds[0]); + (ERC20Mint_BaseToken token1,,) = erc20Mint.tokenData(slicerId, productIds[0], variantId); address tokenAddress = address(token1); // Second configuration - should update existing token erc20Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Updated Token", // name (ignored for existing token) "UT", // symbol (ignored for existing token) @@ -135,7 +143,7 @@ contract ERC20MintTest is RegistryProductActionTest { ); (ERC20Mint_BaseToken token2, bool revertOnMaxSupply2, uint256 tokensPerUnit2) = - erc20Mint.tokenData(slicerId, productIds[0]); + erc20Mint.tokenData(slicerId, productIds[0], variantId); // Token address should be the same assertEq(address(token2), tokenAddress); @@ -157,6 +165,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test Token", // name "TT", // symbol @@ -178,6 +187,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test Token", // name "TT", // symbol @@ -193,6 +203,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[1], + variantId, abi.encode( "Test Token 2", // name "TT2", // symbol @@ -210,15 +221,15 @@ contract ERC20MintTest is RegistryProductActionTest { // Current supply: 800, max supply: 1000 // Available: 200 tokens, with 10 tokens per unit = 20 units max - assertTrue(erc20Mint.isPurchaseAllowed(slicerId, productIds[0], buyer, 1, "", "")); // 10 tokens needed - assertTrue(erc20Mint.isPurchaseAllowed(slicerId, productIds[0], buyer, 10, "", "")); // 100 tokens needed - assertTrue(erc20Mint.isPurchaseAllowed(slicerId, productIds[0], buyer, 20, "", "")); // 200 tokens needed (exactly at limit) - assertFalse(erc20Mint.isPurchaseAllowed(slicerId, productIds[0], buyer, 21, "", "")); // 210 tokens needed (exceeds limit) + assertTrue(erc20Mint.isPurchaseAllowed(slicerId, productIds[0], 0, buyer, 1, "")); // 10 tokens needed + assertTrue(erc20Mint.isPurchaseAllowed(slicerId, productIds[0], 0, buyer, 10, "")); // 100 tokens needed + assertTrue(erc20Mint.isPurchaseAllowed(slicerId, productIds[0], 0, buyer, 20, "")); // 200 tokens needed (exactly at limit) + assertFalse(erc20Mint.isPurchaseAllowed(slicerId, productIds[0], 0, buyer, 21, "")); // 210 tokens needed (exceeds limit) // Test product 2 (unlimited supply) - assertTrue(erc20Mint.isPurchaseAllowed(slicerId, productIds[1], buyer, 1, "", "")); - assertTrue(erc20Mint.isPurchaseAllowed(slicerId, productIds[1], buyer, 1000, "", "")); - assertTrue(erc20Mint.isPurchaseAllowed(slicerId, productIds[1], buyer, type(uint256).max, "", "")); + assertTrue(erc20Mint.isPurchaseAllowed(slicerId, productIds[1], 0, buyer, 1, "")); + assertTrue(erc20Mint.isPurchaseAllowed(slicerId, productIds[1], 0, buyer, 1000, "")); + assertTrue(erc20Mint.isPurchaseAllowed(slicerId, productIds[1], 0, buyer, type(uint256).max, "")); } function testOnProductPurchase() public { @@ -228,6 +239,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test Token 1", // name "TT1", // symbol @@ -242,6 +254,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[1], + variantId, abi.encode( "Test Token 2", // name "TT2", // symbol @@ -255,15 +268,15 @@ contract ERC20MintTest is RegistryProductActionTest { vm.stopPrank(); - (ERC20Mint_BaseToken token1,,) = erc20Mint.tokenData(slicerId, productIds[0]); - (ERC20Mint_BaseToken token2,,) = erc20Mint.tokenData(slicerId, productIds[1]); + (ERC20Mint_BaseToken token1,,) = erc20Mint.tokenData(slicerId, productIds[0], variantId); + (ERC20Mint_BaseToken token2,,) = erc20Mint.tokenData(slicerId, productIds[1], variantId); // Test minting for product 1 uint256 initialBalance1 = token1.balanceOf(buyer); uint256 initialSupply1 = token1.totalSupply(); vm.prank(address(PRODUCTS_MODULE)); - erc20Mint.onProductPurchase(slicerId, productIds[0], buyer, 3, "", ""); + erc20Mint.onProductPurchase(slicerId, productIds[0], variantId, buyer, 3, ""); assertEq(token1.balanceOf(buyer), initialBalance1 + 300); // 3 * 100 assertEq(token1.totalSupply(), initialSupply1 + 300); @@ -273,14 +286,14 @@ contract ERC20MintTest is RegistryProductActionTest { uint256 initialSupply2 = token2.totalSupply(); vm.prank(address(PRODUCTS_MODULE)); - erc20Mint.onProductPurchase(slicerId, productIds[1], buyer2, 5, "", ""); + erc20Mint.onProductPurchase(slicerId, productIds[1], variantId, buyer2, 5, ""); assertEq(token2.balanceOf(buyer2), initialBalance2 + 250); // 5 * 50 assertEq(token2.totalSupply(), initialSupply2 + 250); // Test multiple purchases vm.prank(address(PRODUCTS_MODULE)); - erc20Mint.onProductPurchase(slicerId, productIds[0], buyer3, 2, "", ""); + erc20Mint.onProductPurchase(slicerId, productIds[0], variantId, buyer3, 2, ""); assertEq(token1.balanceOf(buyer3), 200); // 2 * 100 assertEq(token1.totalSupply(), initialSupply1 + 500); // 300 + 200 } @@ -292,6 +305,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test Token", // name "TT", // symbol @@ -306,12 +320,12 @@ contract ERC20MintTest is RegistryProductActionTest { vm.stopPrank(); // This should succeed but not mint tokens (exceeds max supply) - (ERC20Mint_BaseToken token,,) = erc20Mint.tokenData(slicerId, productIds[0]); + (ERC20Mint_BaseToken token,,) = erc20Mint.tokenData(slicerId, productIds[0], variantId); uint256 initialBalance = token.balanceOf(buyer); uint256 initialSupply = token.totalSupply(); vm.prank(address(PRODUCTS_MODULE)); - erc20Mint.onProductPurchase(slicerId, productIds[0], buyer, 2, "", ""); + erc20Mint.onProductPurchase(slicerId, productIds[0], variantId, buyer, 2, ""); // Balance and supply should remain unchanged (mint failed silently) assertEq(token.balanceOf(buyer), initialBalance); @@ -325,6 +339,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test Token", // name "TT", // symbol @@ -340,14 +355,312 @@ contract ERC20MintTest is RegistryProductActionTest { // This should succeed (50 tokens available, need 50) vm.prank(address(PRODUCTS_MODULE)); - erc20Mint.onProductPurchase(slicerId, productIds[0], buyer, 5, "", ""); + erc20Mint.onProductPurchase(slicerId, productIds[0], 0, buyer, 5, ""); - (ERC20Mint_BaseToken token,,) = erc20Mint.tokenData(slicerId, productIds[0]); + (ERC20Mint_BaseToken token,,) = erc20Mint.tokenData(slicerId, productIds[0], variantId); assertEq(token.totalSupply(), 1000); // at max supply // This should revert (no tokens available) vm.expectRevert(RegistryProductAction.NotAllowed.selector); vm.prank(address(PRODUCTS_MODULE)); - erc20Mint.onProductPurchase(slicerId, productIds[0], buyer, 1, "", ""); + erc20Mint.onProductPurchase(slicerId, productIds[0], variantId, buyer, 1, ""); + } + + function testVariantSpecificTokenConfiguration() public { + vm.startPrank(productOwner); + + erc20Mint.configureProduct( + slicerId, + productIds[0], + variantId, + abi.encode( + "Product Token", // name + "PT0", // symbol + 0, // premintAmount + address(0), // premintReceiver + true, // revertOnMaxSupplyReached + 1000, // maxSupply + 10 // tokensPerUnit + ) + ); + + erc20Mint.configureProduct( + slicerId, + productIds[0], + variantIdAlt, + abi.encode( + "Variant Token", // name + "VT1", // symbol + 0, // premintAmount + address(0), // premintReceiver + false, // revertOnMaxSupplyReached + 0, // maxSupply (unlimited) + 5 // tokensPerUnit + ) + ); + + vm.stopPrank(); + + (ERC20Mint_BaseToken productToken, bool revertProduct, uint256 tokensPerUnitProduct) = + erc20Mint.tokenData(slicerId, productIds[0], variantId); + (ERC20Mint_BaseToken variantToken, bool revertVariant, uint256 tokensPerUnitVariant) = + erc20Mint.tokenData(slicerId, productIds[0], variantIdAlt); + + assertTrue(address(productToken) != address(variantToken)); + assertTrue(revertProduct); + assertFalse(revertVariant); + assertEq(tokensPerUnitProduct, 10); + assertEq(tokensPerUnitVariant, 5); + + vm.prank(address(PRODUCTS_MODULE)); + erc20Mint.onProductPurchase(slicerId, productIds[0], variantId, buyer, 2, ""); + + vm.prank(address(PRODUCTS_MODULE)); + erc20Mint.onProductPurchase(slicerId, productIds[0], variantIdAlt, buyer2, 3, ""); + + assertEq(productToken.balanceOf(buyer), 20); + assertEq(variantToken.balanceOf(buyer2), 15); + assertEq(variantToken.balanceOf(buyer), 0); + } + + /*////////////////////////////////////////////////////////////// + PRODUCT TYPE TESTS + //////////////////////////////////////////////////////////////*/ + + function testConfigureProductType() public { + bytes memory params = abi.encode( + "ProductType Token", // name + "PTT", // symbol + 1000, // premintAmount + productOwner, // premintReceiver + true, // revertOnMaxSupplyReached + 10000, // maxSupply + 100 // tokensPerUnit + ); + + vm.expectEmit(true, true, true, true, address(erc20Mint)); + emit HookRegistry.ProductTypeConfigured(slicerId, productTypeId, params); + + vm.prank(productOwner); + erc20Mint.configureProductType(slicerId, productTypeId, params); + + (ERC20Mint_BaseToken token, bool revertOnMaxSupply, uint256 tokensPerUnit) = + erc20Mint.productTypeTokenData(slicerId, productTypeId); + assertEq(revertOnMaxSupply, true); + assertEq(tokensPerUnit, 100); + assertEq(token.name(), "ProductType Token"); + assertEq(token.symbol(), "PTT"); + assertEq(token.maxSupply(), 10000); + assertEq(token.totalSupply(), 1000); // premint amount + assertEq(token.balanceOf(productOwner), 1000); + } + + function testConfigureProductType_AccessControl() public { + vm.expectRevert(abi.encodeWithSelector(SliceContext.NotAuthorized.selector, bytes32(uint256(1 << 1 | 1 << 3)))); + + erc20Mint.configureProductType(slicerId, productTypeId, ""); + } + + function testConfigureProductTypeSchema() public view { + string memory schema = erc20Mint.configureProductTypeSchema(); + + assertEq(schema, erc20Mint.configureProductSchema()); + assertTrue(bytes(schema).length > 0); + } + + function testIsProductTypePurchaseAllowed() public { + vm.prank(productOwner); + erc20Mint.configureProductType( + slicerId, + productTypeId, + abi.encode( + "Test Token", // name + "TT", // symbol + 800, // premintAmount + productOwner, // premintReceiver + true, // revertOnMaxSupplyReached + 1000, // maxSupply + 10 // tokensPerUnit + ) + ); + + // Current supply: 800, max supply: 1000 + // Available: 200 tokens, with 10 tokens per unit = 20 units max + assertTrue(erc20Mint.isProductTypePurchaseAllowed(slicerId, productTypeId, buyer, 1)); // 10 tokens needed + assertTrue(erc20Mint.isProductTypePurchaseAllowed(slicerId, productTypeId, buyer, 20)); // 200 tokens needed (exactly at limit) + assertFalse(erc20Mint.isProductTypePurchaseAllowed(slicerId, productTypeId, buyer, 21)); // 210 tokens needed (exceeds limit) + } + + function testOnProductTypePurchase() public { + vm.prank(productOwner); + erc20Mint.configureProductType( + slicerId, + productTypeId, + abi.encode( + "Test Token", // name + "TT", // symbol + 0, // premintAmount + address(0), // premintReceiver + true, // revertOnMaxSupplyReached + 1000, // maxSupply + 100 // tokensPerUnit + ) + ); + + (ERC20Mint_BaseToken token,,) = erc20Mint.productTypeTokenData(slicerId, productTypeId); + uint256 initialBalance = token.balanceOf(buyer); + uint256 initialSupply = token.totalSupply(); + + vm.prank(address(PRODUCTS_MODULE)); + erc20Mint.onProductTypePurchase(slicerId, productTypeId, buyer, 3); + + assertEq(token.balanceOf(buyer), initialBalance + 300); // 3 * 100 + assertEq(token.totalSupply(), initialSupply + 300); + } + + function testRevert_OnProductTypePurchase_NotPurchase() public { + vm.prank(productOwner); + erc20Mint.configureProductType( + slicerId, + productTypeId, + abi.encode( + "Test Token", // name + "TT", // symbol + 0, // premintAmount + address(0), // premintReceiver + true, // revertOnMaxSupplyReached + 1000, // maxSupply + 100 // tokensPerUnit + ) + ); + + vm.expectRevert(); + erc20Mint.onProductTypePurchase(slicerId, productTypeId, buyer, 1); + } + + /*////////////////////////////////////////////////////////////// + SLICER TESTS + //////////////////////////////////////////////////////////////*/ + + function testConfigureSlicer() public { + bytes memory params = abi.encode( + "Slicer Token", // name + "ST", // symbol + 500, // premintAmount + buyer, // premintReceiver + false, // revertOnMaxSupplyReached + 0, // maxSupply (unlimited) + 50 // tokensPerUnit + ); + + vm.expectEmit(true, true, true, true, address(erc20Mint)); + emit HookRegistry.SlicerConfigured(slicerId, params); + + vm.prank(productOwner); + erc20Mint.configureSlicer(slicerId, params); + + (ERC20Mint_BaseToken token, bool revertOnMaxSupply, uint256 tokensPerUnit) = erc20Mint.slicerTokenData(slicerId); + assertEq(revertOnMaxSupply, false); + assertEq(tokensPerUnit, 50); + assertEq(token.name(), "Slicer Token"); + assertEq(token.symbol(), "ST"); + assertEq(token.maxSupply(), type(uint256).max); + assertEq(token.totalSupply(), 500); // premint amount + assertEq(token.balanceOf(buyer), 500); + } + + function testConfigureSlicer_AccessControl() public { + vm.expectRevert(abi.encodeWithSelector(SliceContext.NotAuthorized.selector, bytes32(uint256(1 << 1 | 1 << 3)))); + + erc20Mint.configureSlicer(slicerId, ""); + } + + function testConfigureSlicerSchema() public view { + string memory schema = erc20Mint.configureSlicerSchema(); + + assertEq(schema, erc20Mint.configureProductSchema()); + assertTrue(bytes(schema).length > 0); + } + + function testIsSlicerPurchaseAllowed() public { + vm.prank(productOwner); + erc20Mint.configureSlicer( + slicerId, + abi.encode( + "Test Token", // name + "TT", // symbol + 0, // premintAmount + address(0), // premintReceiver + false, // revertOnMaxSupplyReached + 0, // maxSupply (unlimited) + 50 // tokensPerUnit + ) + ); + + ProductLineItem[] memory productLineItems = new ProductLineItem[](2); + productLineItems[0] = + ProductLineItem({productId: uint32(productIds[0]), variantId: uint32(variantId), quantity: uint32(5)}); + productLineItems[1] = + ProductLineItem({productId: uint32(productIds[1]), variantId: uint32(variantId), quantity: uint32(3)}); + // Total quantity: 8, tokens needed: 8 * 50 = 400 + + // Unlimited supply, so should always return true + assertTrue(erc20Mint.isSlicerPurchaseAllowed(slicerId, buyer, productLineItems)); + } + + function testOnSlicerPurchase() public { + vm.prank(productOwner); + erc20Mint.configureSlicer( + slicerId, + abi.encode( + "Test Token", // name + "TT", // symbol + 0, // premintAmount + address(0), // premintReceiver + false, // revertOnMaxSupplyReached + 0, // maxSupply (unlimited) + 50 // tokensPerUnit + ) + ); + + ProductLineItem[] memory productLineItems = new ProductLineItem[](2); + productLineItems[0] = + ProductLineItem({productId: uint32(productIds[0]), variantId: uint32(variantId), quantity: uint32(2)}); + productLineItems[1] = + ProductLineItem({productId: uint32(productIds[1]), variantId: uint32(variantId), quantity: uint32(3)}); + // Total quantity: 5, tokens to mint: 5 * 50 = 250 + + (ERC20Mint_BaseToken token,,) = erc20Mint.slicerTokenData(slicerId); + uint256 initialBalance = token.balanceOf(buyer); + uint256 initialSupply = token.totalSupply(); + + vm.prank(address(PRODUCTS_MODULE)); + erc20Mint.onSlicerPurchase(slicerId, buyer, productLineItems); + + assertEq(token.balanceOf(buyer), initialBalance + 250); // 5 * 50 + assertEq(token.totalSupply(), initialSupply + 250); + } + + function testRevert_OnSlicerPurchase_NotPurchase() public { + vm.prank(productOwner); + erc20Mint.configureSlicer( + slicerId, + abi.encode( + "Test Token", // name + "TT", // symbol + 0, // premintAmount + address(0), // premintReceiver + false, // revertOnMaxSupplyReached + 0, // maxSupply (unlimited) + 50 // tokensPerUnit + ) + ); + + ProductLineItem[] memory productLineItems = new ProductLineItem[](1); + productLineItems[0] = + ProductLineItem({productId: uint32(productIds[0]), variantId: uint32(variantId), quantity: uint32(5)}); + + vm.expectRevert(); + erc20Mint.onSlicerPurchase(slicerId, buyer, productLineItems); } } diff --git a/test/actions/ERC721Mint/ERC721Mint.t.sol b/test/actions/ERC721Mint/ERC721Mint.t.sol index 9a25bc2..b592afd 100644 --- a/test/actions/ERC721Mint/ERC721Mint.t.sol +++ b/test/actions/ERC721Mint/ERC721Mint.t.sol @@ -5,10 +5,13 @@ import {RegistryProductActionTest} from "@test/utils/RegistryProductActionTest.s import {ERC721Mint} from "@/hooks/actions/ERC721Mint/ERC721Mint.sol"; import {ERC721Data} from "@/hooks/actions/ERC721Mint/types/ERC721Data.sol"; import {ERC721Mint_BaseToken, MAX_ROYALTY} from "@/hooks/actions/ERC721Mint/utils/ERC721Mint_BaseToken.sol"; - -import {console2} from "forge-std/console2.sol"; +import {HookRegistry, ProductLineItem} from "@/utils/RegistryAction.sol"; +import {SliceContext} from "@/utils/RegistryProductAction.sol"; uint256 constant slicerId = 0; +uint256 constant variantId = 0; +uint256 constant variantIdAlt = 1; +uint256 constant productTypeId = 2; contract ERC721MintTest is RegistryProductActionTest { ERC721Mint erc721Mint; @@ -27,6 +30,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test NFT 1", // name "TNT1", // symbol @@ -43,6 +47,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[1], + variantId, abi.encode( "Test NFT 2", // name "TNT2", // symbol @@ -59,6 +64,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[2], + variantId, abi.encode( "Test NFT 3", // name "TNT3", // symbol @@ -74,7 +80,8 @@ contract ERC721MintTest is RegistryProductActionTest { vm.stopPrank(); // Verify tokenData is set correctly - (ERC721Mint_BaseToken token1, bool revertOnMaxSupply1) = erc721Mint.tokenData(slicerId, productIds[0]); + (ERC721Mint_BaseToken token1, bool revertOnMaxSupply1) = + erc721Mint.tokenData(slicerId, productIds[0], variantId); assertEq(revertOnMaxSupply1, true); assertEq(token1.name(), "Test NFT 1"); assertEq(token1.symbol(), "TNT1"); @@ -85,7 +92,8 @@ contract ERC721MintTest is RegistryProductActionTest { assertEq(token1.baseURI_(), "https://api.example.com/metadata/"); assertEq(token1.tokenURI_(), "https://api.example.com/fallback.json"); - (ERC721Mint_BaseToken token2, bool revertOnMaxSupply2) = erc721Mint.tokenData(slicerId, productIds[1]); + (ERC721Mint_BaseToken token2, bool revertOnMaxSupply2) = + erc721Mint.tokenData(slicerId, productIds[1], variantId); assertEq(revertOnMaxSupply2, false); assertEq(token2.name(), "Test NFT 2"); assertEq(token2.symbol(), "TNT2"); @@ -96,7 +104,8 @@ contract ERC721MintTest is RegistryProductActionTest { assertEq(token2.baseURI_(), ""); assertEq(token2.tokenURI_(), "https://api.example.com/single.json"); - (ERC721Mint_BaseToken token3, bool revertOnMaxSupply3) = erc721Mint.tokenData(slicerId, productIds[2]); + (ERC721Mint_BaseToken token3, bool revertOnMaxSupply3) = + erc721Mint.tokenData(slicerId, productIds[2], variantId); assertEq(revertOnMaxSupply3, true); assertEq(token3.maxSupply(), 100); assertEq(token3.royaltyReceiver(), buyer); @@ -112,6 +121,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test NFT", // name "TNT", // symbol @@ -124,13 +134,14 @@ contract ERC721MintTest is RegistryProductActionTest { ) ); - (ERC721Mint_BaseToken token1,) = erc721Mint.tokenData(slicerId, productIds[0]); + (ERC721Mint_BaseToken token1,) = erc721Mint.tokenData(slicerId, productIds[0], variantId); address tokenAddress = address(token1); // Second configuration - should update existing token erc721Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Updated NFT", // name (ignored for existing token) "UNT", // symbol (ignored for existing token) @@ -143,7 +154,8 @@ contract ERC721MintTest is RegistryProductActionTest { ) ); - (ERC721Mint_BaseToken token2, bool revertOnMaxSupply2) = erc721Mint.tokenData(slicerId, productIds[0]); + (ERC721Mint_BaseToken token2, bool revertOnMaxSupply2) = + erc721Mint.tokenData(slicerId, productIds[0], variantId); // Token address should be the same assertEq(address(token2), tokenAddress); @@ -167,6 +179,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test NFT", // name "TNT", // symbol @@ -187,6 +200,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test NFT 1", // name "TNT1", // symbol @@ -202,6 +216,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[1], + variantId, abi.encode( "Test NFT 2", // name "TNT2", // symbol @@ -216,15 +231,15 @@ contract ERC721MintTest is RegistryProductActionTest { vm.stopPrank(); - (ERC721Mint_BaseToken token1,) = erc721Mint.tokenData(slicerId, productIds[0]); - (ERC721Mint_BaseToken token2,) = erc721Mint.tokenData(slicerId, productIds[1]); + (ERC721Mint_BaseToken token1,) = erc721Mint.tokenData(slicerId, productIds[0], variantId); + (ERC721Mint_BaseToken token2,) = erc721Mint.tokenData(slicerId, productIds[1], variantId); // Test minting for product 1 uint256 initialBalance1 = token1.balanceOf(buyer); uint256 initialSupply1 = token1.totalSupply(); vm.prank(address(PRODUCTS_MODULE)); - erc721Mint.onProductPurchase(slicerId, productIds[0], buyer, 3, "", ""); + erc721Mint.onProductPurchase(slicerId, productIds[0], variantId, buyer, 3, ""); assertEq(token1.balanceOf(buyer), initialBalance1 + 3); assertEq(token1.totalSupply(), initialSupply1 + 3); @@ -234,14 +249,14 @@ contract ERC721MintTest is RegistryProductActionTest { uint256 initialSupply2 = token2.totalSupply(); vm.prank(address(PRODUCTS_MODULE)); - erc721Mint.onProductPurchase(slicerId, productIds[1], buyer2, 5, "", ""); + erc721Mint.onProductPurchase(slicerId, productIds[1], variantId, buyer2, 5, ""); assertEq(token2.balanceOf(buyer2), initialBalance2 + 5); assertEq(token2.totalSupply(), initialSupply2 + 5); // Test multiple purchases vm.prank(address(PRODUCTS_MODULE)); - erc721Mint.onProductPurchase(slicerId, productIds[0], buyer3, 2, "", ""); + erc721Mint.onProductPurchase(slicerId, productIds[0], variantId, buyer3, 2, ""); assertEq(token1.balanceOf(buyer3), 2); assertEq(token1.totalSupply(), initialSupply1 + 5); // 3 + 2 } @@ -253,6 +268,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test NFT", // name "TNT", // symbol @@ -265,16 +281,16 @@ contract ERC721MintTest is RegistryProductActionTest { ) ); - (ERC721Mint_BaseToken token,) = erc721Mint.tokenData(slicerId, productIds[0]); + (ERC721Mint_BaseToken token,) = erc721Mint.tokenData(slicerId, productIds[0], variantId); // First purchase - should succeed vm.prank(address(PRODUCTS_MODULE)); - erc721Mint.onProductPurchase(slicerId, productIds[0], buyer, 3, "", ""); + erc721Mint.onProductPurchase(slicerId, productIds[0], variantId, buyer, 3, ""); assertEq(token.totalSupply(), 3); // Second purchase - should succeed vm.prank(address(PRODUCTS_MODULE)); - erc721Mint.onProductPurchase(slicerId, productIds[0], buyer2, 2, "", ""); + erc721Mint.onProductPurchase(slicerId, productIds[0], variantId, buyer2, 2, ""); assertEq(token.totalSupply(), 5); // Third purchase - exceeds max supply but should not revert (mint will fail silently) @@ -282,7 +298,7 @@ contract ERC721MintTest is RegistryProductActionTest { uint256 supplyBefore = token.totalSupply(); vm.prank(address(PRODUCTS_MODULE)); - erc721Mint.onProductPurchase(slicerId, productIds[0], buyer3, 2, "", ""); + erc721Mint.onProductPurchase(slicerId, productIds[0], variantId, buyer3, 2, ""); // Balance and supply should remain unchanged (mint failed silently) assertEq(token.balanceOf(buyer3), balanceBefore); @@ -296,6 +312,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test NFT", // name "TNT", // symbol @@ -310,20 +327,20 @@ contract ERC721MintTest is RegistryProductActionTest { // First purchase - should succeed vm.prank(address(PRODUCTS_MODULE)); - erc721Mint.onProductPurchase(slicerId, productIds[0], buyer, 3, "", ""); + erc721Mint.onProductPurchase(slicerId, productIds[0], 0, buyer, 3, ""); - (ERC721Mint_BaseToken token,) = erc721Mint.tokenData(slicerId, productIds[0]); + (ERC721Mint_BaseToken token,) = erc721Mint.tokenData(slicerId, productIds[0], variantId); assertEq(token.totalSupply(), 3); // Second purchase - should succeed (exactly at max supply) vm.prank(address(PRODUCTS_MODULE)); - erc721Mint.onProductPurchase(slicerId, productIds[0], buyer2, 2, "", ""); + erc721Mint.onProductPurchase(slicerId, productIds[0], variantId, buyer2, 2, ""); assertEq(token.totalSupply(), 5); // Third purchase - should revert (exceeds max supply) vm.expectRevert(ERC721Mint.MaxSupplyExceeded.selector); vm.prank(address(PRODUCTS_MODULE)); - erc721Mint.onProductPurchase(slicerId, productIds[0], buyer3, 1, "", ""); + erc721Mint.onProductPurchase(slicerId, productIds[0], variantId, buyer3, 1, ""); } function testTokenURI() public { @@ -333,6 +350,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test NFT", // name "TNT", // symbol @@ -349,6 +367,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[1], + variantId, abi.encode( "Test NFT 2", // name "TNT2", // symbol @@ -363,15 +382,15 @@ contract ERC721MintTest is RegistryProductActionTest { vm.stopPrank(); - (ERC721Mint_BaseToken token1,) = erc721Mint.tokenData(slicerId, productIds[0]); - (ERC721Mint_BaseToken token2,) = erc721Mint.tokenData(slicerId, productIds[1]); + (ERC721Mint_BaseToken token1,) = erc721Mint.tokenData(slicerId, productIds[0], variantId); + (ERC721Mint_BaseToken token2,) = erc721Mint.tokenData(slicerId, productIds[1], variantId); // Mint some tokens vm.prank(address(PRODUCTS_MODULE)); - erc721Mint.onProductPurchase(slicerId, productIds[0], buyer, 2, "", ""); + erc721Mint.onProductPurchase(slicerId, productIds[0], variantId, buyer, 2, ""); vm.prank(address(PRODUCTS_MODULE)); - erc721Mint.onProductPurchase(slicerId, productIds[1], buyer, 1, "", ""); + erc721Mint.onProductPurchase(slicerId, productIds[1], variantId, buyer, 1, ""); // Test token URIs for product 1 (with baseURI) assertEq(token1.tokenURI(0), "https://api.example.com/metadata/0"); @@ -381,11 +400,69 @@ contract ERC721MintTest is RegistryProductActionTest { assertEq(token2.tokenURI(0), "https://single-token.json"); } + function testVariantSpecificTokenConfiguration() public { + vm.startPrank(productOwner); + + erc721Mint.configureProduct( + slicerId, + productIds[0], + variantId, + abi.encode( + "Variant Zero", // name + "V0", // symbol + productOwner, // royaltyReceiver + 500, // royaltyFraction (5%) + "ipfs://base0/", // baseURI + "", // tokenURI + true, // revertOnMaxSupplyReached + 100 // maxSupply + ) + ); + + erc721Mint.configureProduct( + slicerId, + productIds[0], + variantIdAlt, + abi.encode( + "Variant One", // name + "V1", // symbol + buyer, // royaltyReceiver + 0, // royaltyFraction (0%) + "", // baseURI + "https://static.example/token.json", // tokenURI + false, // revertOnMaxSupplyReached + 0 // maxSupply (unlimited) + ) + ); + + vm.stopPrank(); + + (ERC721Mint_BaseToken productToken, bool revertProduct) = + erc721Mint.tokenData(slicerId, productIds[0], variantId); + (ERC721Mint_BaseToken variantToken, bool revertVariant) = + erc721Mint.tokenData(slicerId, productIds[0], variantIdAlt); + + assertTrue(address(productToken) != address(variantToken)); + assertTrue(revertProduct); + assertFalse(revertVariant); + assertEq(productToken.royaltyReceiver(), productOwner); + assertEq(variantToken.royaltyReceiver(), buyer); + assertEq(productToken.baseURI_(), "ipfs://base0/"); + assertEq(variantToken.tokenURI_(), "https://static.example/token.json"); + + vm.prank(address(PRODUCTS_MODULE)); + erc721Mint.onProductPurchase(slicerId, productIds[0], variantIdAlt, buyer2, 2, ""); + + assertEq(variantToken.balanceOf(buyer2), 2); + assertEq(productToken.balanceOf(buyer2), 0); + } + function testRoyaltyInfo() public { vm.prank(productOwner); erc721Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test NFT", // name "TNT", // symbol @@ -398,7 +475,7 @@ contract ERC721MintTest is RegistryProductActionTest { ) ); - (ERC721Mint_BaseToken token,) = erc721Mint.tokenData(slicerId, productIds[0]); + (ERC721Mint_BaseToken token,) = erc721Mint.tokenData(slicerId, productIds[0], variantId); // Test royalty calculation (address receiver, uint256 royaltyAmount) = token.royaltyInfo(0, 1000); @@ -409,4 +486,292 @@ contract ERC721MintTest is RegistryProductActionTest { assertEq(receiver, productOwner); assertEq(royaltyAmount, 100); // 5% of 2000 } + + /*////////////////////////////////////////////////////////////// + PRODUCT TYPE TESTS + //////////////////////////////////////////////////////////////*/ + + function testConfigureProductType() public { + bytes memory params = abi.encode( + "ProductType NFT", // name + "PTN", // symbol + productOwner, // royaltyReceiver + 500, // royaltyFraction (5%) + "https://api.example.com/metadata/", // baseURI + "https://api.example.com/fallback.json", // tokenURI + true, // revertOnMaxSupplyReached + 1000 // maxSupply + ); + + vm.expectEmit(true, true, true, true, address(erc721Mint)); + emit HookRegistry.ProductTypeConfigured(slicerId, productTypeId, params); + + vm.prank(productOwner); + erc721Mint.configureProductType(slicerId, productTypeId, params); + + (ERC721Mint_BaseToken token, bool revertOnMaxSupply) = erc721Mint.productTypeTokenData(slicerId, productTypeId); + assertEq(revertOnMaxSupply, true); + assertEq(token.name(), "ProductType NFT"); + assertEq(token.symbol(), "PTN"); + assertEq(token.maxSupply(), 1000); + assertEq(token.totalSupply(), 0); + assertEq(token.royaltyReceiver(), productOwner); + assertEq(token.royaltyFraction(), 500); + assertEq(token.baseURI_(), "https://api.example.com/metadata/"); + assertEq(token.tokenURI_(), "https://api.example.com/fallback.json"); + } + + function testConfigureProductType_AccessControl() public { + vm.expectRevert(abi.encodeWithSelector(SliceContext.NotAuthorized.selector, bytes32(uint256(1 << 1 | 1 << 3)))); + + erc721Mint.configureProductType(slicerId, productTypeId, ""); + } + + function testConfigureProductTypeSchema() public view { + string memory schema = erc721Mint.configureProductTypeSchema(); + + assertEq(schema, erc721Mint.configureProductSchema()); + assertTrue(bytes(schema).length > 0); + } + + function testOnProductTypePurchase() public { + vm.prank(productOwner); + erc721Mint.configureProductType( + slicerId, + productTypeId, + abi.encode( + "Test NFT", // name + "TNT", // symbol + productOwner, // royaltyReceiver + 500, // royaltyFraction + "https://api.example.com/", // baseURI + "", // tokenURI + true, // revertOnMaxSupplyReached + 1000 // maxSupply + ) + ); + + (ERC721Mint_BaseToken token,) = erc721Mint.productTypeTokenData(slicerId, productTypeId); + uint256 initialBalance = token.balanceOf(buyer); + uint256 initialSupply = token.totalSupply(); + + vm.prank(address(PRODUCTS_MODULE)); + erc721Mint.onProductTypePurchase(slicerId, productTypeId, buyer, 3); + + assertEq(token.balanceOf(buyer), initialBalance + 3); + assertEq(token.totalSupply(), initialSupply + 3); + } + + function testRevert_OnProductTypePurchase_NotPurchase() public { + vm.prank(productOwner); + erc721Mint.configureProductType( + slicerId, + productTypeId, + abi.encode( + "Test NFT", // name + "TNT", // symbol + productOwner, // royaltyReceiver + 500, // royaltyFraction + "https://api.example.com/", // baseURI + "", // tokenURI + true, // revertOnMaxSupplyReached + 1000 // maxSupply + ) + ); + + vm.expectRevert(); + erc721Mint.onProductTypePurchase(slicerId, productTypeId, buyer, 1); + } + + function testRevert_OnProductTypePurchase_MaxSupplyReached() public { + vm.prank(productOwner); + erc721Mint.configureProductType( + slicerId, + productTypeId, + abi.encode( + "Test NFT", // name + "TNT", // symbol + productOwner, // royaltyReceiver + 0, // royaltyFraction + "", // baseURI + "", // tokenURI + true, // revertOnMaxSupplyReached + 5 // maxSupply + ) + ); + + // First purchase - should succeed + vm.prank(address(PRODUCTS_MODULE)); + erc721Mint.onProductTypePurchase(slicerId, productTypeId, buyer, 3); + + (ERC721Mint_BaseToken token,) = erc721Mint.productTypeTokenData(slicerId, productTypeId); + assertEq(token.totalSupply(), 3); + + // Second purchase - should succeed (exactly at max supply) + vm.prank(address(PRODUCTS_MODULE)); + erc721Mint.onProductTypePurchase(slicerId, productTypeId, buyer2, 2); + assertEq(token.totalSupply(), 5); + + // Third purchase - should revert (exceeds max supply) + vm.expectRevert(ERC721Mint.MaxSupplyExceeded.selector); + vm.prank(address(PRODUCTS_MODULE)); + erc721Mint.onProductTypePurchase(slicerId, productTypeId, buyer3, 1); + } + + /*////////////////////////////////////////////////////////////// + SLICER TESTS + //////////////////////////////////////////////////////////////*/ + + function testConfigureSlicer() public { + bytes memory params = abi.encode( + "Slicer NFT", // name + "SN", // symbol + buyer, // royaltyReceiver + 0, // royaltyFraction + "", // baseURI + "https://slicer.json", // tokenURI + false, // revertOnMaxSupplyReached + 0 // maxSupply (unlimited) + ); + + vm.expectEmit(true, true, true, true, address(erc721Mint)); + emit HookRegistry.SlicerConfigured(slicerId, params); + + vm.prank(productOwner); + erc721Mint.configureSlicer(slicerId, params); + + (ERC721Mint_BaseToken token, bool revertOnMaxSupply) = erc721Mint.slicerTokenData(slicerId); + assertEq(revertOnMaxSupply, false); + assertEq(token.name(), "Slicer NFT"); + assertEq(token.symbol(), "SN"); + assertEq(token.maxSupply(), type(uint256).max); + assertEq(token.totalSupply(), 0); + assertEq(token.royaltyReceiver(), buyer); + assertEq(token.royaltyFraction(), 0); + assertEq(token.baseURI_(), ""); + assertEq(token.tokenURI_(), "https://slicer.json"); + } + + function testConfigureSlicer_AccessControl() public { + vm.expectRevert(abi.encodeWithSelector(SliceContext.NotAuthorized.selector, bytes32(uint256(1 << 1 | 1 << 3)))); + + erc721Mint.configureSlicer(slicerId, ""); + } + + function testConfigureSlicerSchema() public view { + string memory schema = erc721Mint.configureSlicerSchema(); + + assertEq(schema, erc721Mint.configureProductSchema()); + assertTrue(bytes(schema).length > 0); + } + + function testOnSlicerPurchase() public { + vm.prank(productOwner); + erc721Mint.configureSlicer( + slicerId, + abi.encode( + "Test NFT", // name + "TNT", // symbol + productOwner, // royaltyReceiver + 500, // royaltyFraction + "https://api.example.com/", // baseURI + "", // tokenURI + false, // revertOnMaxSupplyReached + 0 // maxSupply (unlimited) + ) + ); + + ProductLineItem[] memory productLineItems = new ProductLineItem[](2); + productLineItems[0] = + ProductLineItem({productId: uint32(productIds[0]), variantId: uint32(variantId), quantity: uint32(2)}); + productLineItems[1] = + ProductLineItem({productId: uint32(productIds[1]), variantId: uint32(variantId), quantity: uint32(3)}); + // Total quantity: 5 + + (ERC721Mint_BaseToken token,) = erc721Mint.slicerTokenData(slicerId); + uint256 initialBalance = token.balanceOf(buyer); + uint256 initialSupply = token.totalSupply(); + + vm.prank(address(PRODUCTS_MODULE)); + erc721Mint.onSlicerPurchase(slicerId, buyer, productLineItems); + + assertEq(token.balanceOf(buyer), initialBalance + 5); // 2 + 3 + assertEq(token.totalSupply(), initialSupply + 5); + } + + function testRevert_OnSlicerPurchase_NotPurchase() public { + vm.prank(productOwner); + erc721Mint.configureSlicer( + slicerId, + abi.encode( + "Test NFT", // name + "TNT", // symbol + productOwner, // royaltyReceiver + 500, // royaltyFraction + "https://api.example.com/", // baseURI + "", // tokenURI + false, // revertOnMaxSupplyReached + 0 // maxSupply (unlimited) + ) + ); + + ProductLineItem[] memory productLineItems = new ProductLineItem[](1); + productLineItems[0] = + ProductLineItem({productId: uint32(productIds[0]), variantId: uint32(variantId), quantity: uint32(5)}); + + vm.expectRevert(); + erc721Mint.onSlicerPurchase(slicerId, buyer, productLineItems); + } + + function testRevert_OnSlicerPurchase_MaxSupplyReached() public { + vm.prank(productOwner); + erc721Mint.configureSlicer( + slicerId, + abi.encode( + "Test NFT", // name + "TNT", // symbol + productOwner, // royaltyReceiver + 0, // royaltyFraction + "", // baseURI + "", // tokenURI + true, // revertOnMaxSupplyReached + 5 // maxSupply + ) + ); + + ProductLineItem[] memory productLineItems1 = new ProductLineItem[](1); + productLineItems1[0] = + ProductLineItem({productId: uint32(productIds[0]), variantId: uint32(variantId), quantity: uint32(3)}); + + // First purchase - should succeed + vm.prank(address(PRODUCTS_MODULE)); + erc721Mint.onSlicerPurchase(slicerId, buyer, productLineItems1); + + (ERC721Mint_BaseToken token,) = erc721Mint.slicerTokenData(slicerId); + assertEq(token.totalSupply(), 3); + + ProductLineItem[] memory productLineItems2 = new ProductLineItem[](1); + productLineItems2[0] = + ProductLineItem({productId: uint32(productIds[0]), variantId: uint32(variantId), quantity: uint32(2)}); + + // Second purchase - should succeed (exactly at max supply) + vm.prank(address(PRODUCTS_MODULE)); + erc721Mint.onSlicerPurchase(slicerId, buyer2, productLineItems2); + assertEq(token.totalSupply(), 5); + + ProductLineItem[] memory productLineItems3 = new ProductLineItem[](1); + productLineItems3[0] = + ProductLineItem({productId: uint32(productIds[0]), variantId: uint32(variantId), quantity: uint32(1)}); + + // Third purchase - should fail silently (slicer actions don't revert on max supply) + uint256 balanceBefore = token.balanceOf(buyer3); + uint256 supplyBefore = token.totalSupply(); + + vm.prank(address(PRODUCTS_MODULE)); + erc721Mint.onSlicerPurchase(slicerId, buyer3, productLineItems3); + + // Balance and supply should remain unchanged (mint failed silently) + assertEq(token.balanceOf(buyer3), balanceBefore); + assertEq(token.totalSupply(), supplyBefore); + } } diff --git a/test/actions/NFTGated/NFTGated.t.sol b/test/actions/NFTGated/NFTGated.t.sol index 10e2b23..f65da29 100644 --- a/test/actions/NFTGated/NFTGated.t.sol +++ b/test/actions/NFTGated/NFTGated.t.sol @@ -1,15 +1,20 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; +import {ERC20} from "@openzeppelin-4.8.0/token/ERC20/ERC20.sol"; import {RegistryProductActionTest} from "@test/utils/RegistryProductActionTest.sol"; -import {MockNFTGated} from "./mocks/MockNFTGated.sol"; -import {NFTGates, NFTGate, NftType} from "@/hooks/actions/NFTGated/NFTGated.sol"; +import {MockNFTGated, NFTGated} from "./mocks/MockNFTGated.sol"; +import {NFTGate, NftType} from "@/hooks/actions/NFTGated/NFTGated.sol"; +import {NFTGates} from "@/hooks/actions/NFTGated/types/NFTGate.sol"; import {MockERC721} from "@test/utils/mocks/MockERC721.sol"; import {MockERC1155} from "@test/utils/mocks/MockERC1155.sol"; - -import {console2} from "forge-std/console2.sol"; +import {HookRegistry, ProductLineItem} from "@/utils/RegistryAction.sol"; +import {SliceContext} from "@/utils/RegistryProductAction.sol"; uint256 constant slicerId = 0; +uint256 constant variantId = 0; +uint256 constant variantIdAlt = 1; +uint256 constant productTypeId = 2; contract NFTGatedTest is RegistryProductActionTest { MockNFTGated nftGated; @@ -28,8 +33,8 @@ contract NFTGatedTest is RegistryProductActionTest { vm.startPrank(productOwner); for (uint256 i = 0; i < productIds.length; i++) { - nftGated.configureProduct(slicerId, productIds[i], abi.encode(nftGates[i])); - assertEq(nftGated.nftGates(slicerId, productIds[i]), nftGates[i].minOwned); + nftGated.configureProduct(slicerId, productIds[i], variantId, encodeNFTGates(nftGates[i])); + assertEq(nftGated.nftGates(slicerId, productIds[i], variantId), nftGates[i].minOwned); } vm.stopPrank(); } @@ -39,14 +44,14 @@ contract NFTGatedTest is RegistryProductActionTest { vm.startPrank(productOwner); - nftGated.configureProduct(slicerId, productIds[2], abi.encode(nftGates[2])); - assertEq(nftGated.gates(slicerId, productIds[2])[0].nft, address(nft721)); - assertEq(nftGated.gates(slicerId, productIds[2])[1].nft, address(nft1155)); - assertEq(nftGated.gates(slicerId, productIds[2]).length, 2); + nftGated.configureProduct(slicerId, productIds[2], variantId, encodeNFTGates(nftGates[2])); + assertEq(nftGated.gates(slicerId, productIds[2], variantId)[0].nft, address(nft721)); + assertEq(nftGated.gates(slicerId, productIds[2], variantId)[1].nft, address(nft1155)); + assertEq(nftGated.gates(slicerId, productIds[2], variantId).length, 2); - nftGated.configureProduct(slicerId, productIds[2], abi.encode(nftGates[1])); - assertEq(nftGated.gates(slicerId, productIds[2])[0].nft, address(nft1155)); - assertEq(nftGated.gates(slicerId, productIds[2]).length, 1); + nftGated.configureProduct(slicerId, productIds[2], variantId, encodeNFTGates(nftGates[1])); + assertEq(nftGated.gates(slicerId, productIds[2], variantId)[0].nft, address(nft1155)); + assertEq(nftGated.gates(slicerId, productIds[2], variantId).length, 1); vm.stopPrank(); } @@ -56,7 +61,7 @@ contract NFTGatedTest is RegistryProductActionTest { vm.startPrank(productOwner); for (uint256 i = 0; i < productIds.length; i++) { - nftGated.configureProduct(slicerId, productIds[i], abi.encode(nftGates[i])); + nftGated.configureProduct(slicerId, productIds[i], variantId, encodeNFTGates(nftGates[i])); } vm.stopPrank(); @@ -67,22 +72,307 @@ contract NFTGatedTest is RegistryProductActionTest { nft1155.mint(buyer3); // buyer should be able to purchase all products - assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[0], buyer, 0, "", "")); - assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[1], buyer, 0, "", "")); - assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[2], buyer, 0, "", "")); - assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[3], buyer, 0, "", "")); + assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[0], variantId, buyer, 0, "")); + assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[1], variantId, buyer, 0, "")); + assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[2], variantId, buyer, 0, "")); + assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[3], variantId, buyer, 0, "")); // buyer2 should be able to purchase all products except product 2 and 4 - assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[0], buyer2, 0, "", "")); - assertFalse(nftGated.isPurchaseAllowed(slicerId, productIds[1], buyer2, 0, "", "")); - assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[2], buyer2, 0, "", "")); - assertFalse(nftGated.isPurchaseAllowed(slicerId, productIds[3], buyer2, 0, "", "")); + assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[0], variantId, buyer2, 0, "")); + assertFalse(nftGated.isPurchaseAllowed(slicerId, productIds[1], variantId, buyer2, 0, "")); + assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[2], variantId, buyer2, 0, "")); + assertFalse(nftGated.isPurchaseAllowed(slicerId, productIds[3], variantId, buyer2, 0, "")); // buyer3 should be able to purchase all products except product 1 and 4 - assertFalse(nftGated.isPurchaseAllowed(slicerId, productIds[0], buyer3, 0, "", "")); - assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[1], buyer3, 0, "", "")); - assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[2], buyer3, 0, "", "")); - assertFalse(nftGated.isPurchaseAllowed(slicerId, productIds[3], buyer3, 0, "", "")); + assertFalse(nftGated.isPurchaseAllowed(slicerId, productIds[0], variantId, buyer3, 0, "")); + assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[1], variantId, buyer3, 0, "")); + assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[2], variantId, buyer3, 0, "")); + assertFalse(nftGated.isPurchaseAllowed(slicerId, productIds[3], variantId, buyer3, 0, "")); + } + + function testIsPurchaseAllowed_variantSpecificConfiguration() public { + NFTGate[] memory productVariantGates = new NFTGate[](1); + productVariantGates[0] = NFTGate(address(nft721), NftType.ERC721, 1, 1); + NFTGate[] memory variantSpecificGates = new NFTGate[](1); + variantSpecificGates[0] = NFTGate(address(nft1155), NftType.ERC1155, 1, 1); + + vm.startPrank(productOwner); + nftGated.configureProduct(slicerId, productIds[0], variantId, encodeNFTGates(NFTGates(productVariantGates, 1))); + nftGated.configureProduct( + slicerId, productIds[0], variantIdAlt, encodeNFTGates(NFTGates(variantSpecificGates, 1)) + ); + vm.stopPrank(); + + nft721.mint(buyer); + assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[0], variantId, buyer, 0, "")); + assertFalse(nftGated.isPurchaseAllowed(slicerId, productIds[0], variantIdAlt, buyer, 0, "")); + + nft1155.mint(buyer); + assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[0], variantIdAlt, buyer, 0, "")); + + nft1155.mint(buyer2); + assertFalse(nftGated.isPurchaseAllowed(slicerId, productIds[0], variantId, buyer2, 0, "")); + assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[0], variantIdAlt, buyer2, 0, "")); + } + + function testRevert_ConfigureProduct_InvalidNFT() public { + NFTGates[] memory nftGates = new NFTGates[](1); + + // Only 721 is required + NFTGate[] memory gates1 = new NFTGate[](1); + gates1[0] = NFTGate(address(new TestERC20()), NftType.ERC721, 1, 1); + nftGates[0] = NFTGates(gates1, 1); + + vm.startPrank(productOwner); + + vm.expectRevert(NFTGated.InvalidNFT.selector); + nftGated.configureProduct(slicerId, productIds[0], variantId, encodeNFTGates(nftGates[0])); + + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////// + PRODUCT TYPE TESTS + //////////////////////////////////////////////////////////////*/ + + function testConfigureProductType() public { + NFTGates[] memory nftGates = generateNFTGates(); + + bytes memory params = encodeNFTGates(nftGates[2]); + + vm.expectEmit(true, true, true, true, address(nftGated)); + emit HookRegistry.ProductTypeConfigured(slicerId, productTypeId, params); + + vm.prank(productOwner); + nftGated.configureProductType(slicerId, productTypeId, params); + + assertEq(nftGated.productTypeMinOwned(slicerId, productTypeId), 1); + assertEq(nftGated.productTypeGates(slicerId, productTypeId)[0].nft, address(nft721)); + assertEq(nftGated.productTypeGates(slicerId, productTypeId)[1].nft, address(nft1155)); + assertEq(nftGated.productTypeGates(slicerId, productTypeId).length, 2); + + // Buyer doesn't have NFTs yet + assertFalse(nftGated.isProductTypePurchaseAllowed(slicerId, productTypeId, buyer, 0)); + + // Buyer has only ERC721 + nft721.mint(buyer); + assertTrue(nftGated.isProductTypePurchaseAllowed(slicerId, productTypeId, buyer, 0)); + + // Buyer2 has only ERC1155 + nft1155.mint(buyer2); + assertTrue(nftGated.isProductTypePurchaseAllowed(slicerId, productTypeId, buyer2, 0)); + } + + function testConfigureProductType_AccessControl() public { + vm.expectRevert(abi.encodeWithSelector(SliceContext.NotAuthorized.selector, bytes32(uint256(1 << 1 | 1 << 3)))); + + nftGated.configureProductType(slicerId, productTypeId, ""); + } + + function testConfigureProductTypeSchema() public view { + string memory schema = nftGated.configureProductTypeSchema(); + + assertEq(schema, nftGated.configureProductSchema()); + assertTrue(bytes(schema).length > 0); + } + + function testIsProductTypePurchaseAllowed_NoGates() public { + vm.prank(productOwner); + NFTGates memory emptyGates = NFTGates(new NFTGate[](0), 0); + nftGated.configureProductType(slicerId, productTypeId, encodeNFTGates(emptyGates)); + + assertTrue(nftGated.isProductTypePurchaseAllowed(slicerId, productTypeId, buyer2, 0)); + } + + function testConfigureProductType__Edit_Remove() public { + NFTGates[] memory nftGates = generateNFTGates(); + + vm.prank(productOwner); + nftGated.configureProductType(slicerId, productTypeId, encodeNFTGates(nftGates[2])); + + nft721.mint(buyer); + assertTrue(nftGated.isProductTypePurchaseAllowed(slicerId, productTypeId, buyer, 0)); + + // Remove gates + vm.prank(productOwner); + NFTGates memory emptyGates = NFTGates(new NFTGate[](0), 0); + nftGated.configureProductType(slicerId, productTypeId, encodeNFTGates(emptyGates)); + + assertTrue(nftGated.isProductTypePurchaseAllowed(slicerId, productTypeId, buyer, 0)); + assertEq(nftGated.productTypeGates(slicerId, productTypeId).length, 0); + } + + function testOnProductTypePurchase() public { + NFTGates[] memory nftGates = generateNFTGates(); + + vm.prank(productOwner); + nftGated.configureProductType(slicerId, productTypeId, encodeNFTGates(nftGates[0])); + + // Buyer doesn't have NFT - should revert + vm.expectRevert(); + vm.prank(address(PRODUCTS_MODULE)); + nftGated.onProductTypePurchase(slicerId, productTypeId, buyer, 1); + + // Buyer has NFT - should succeed + nft721.mint(buyer); + vm.prank(address(PRODUCTS_MODULE)); + nftGated.onProductTypePurchase(slicerId, productTypeId, buyer, 1); + } + + function testRevert_OnProductTypePurchase_NotPurchase() public { + NFTGates[] memory nftGates = generateNFTGates(); + + vm.prank(productOwner); + nftGated.configureProductType(slicerId, productTypeId, encodeNFTGates(nftGates[0])); + + nft721.mint(buyer); + + vm.expectRevert(); + nftGated.onProductTypePurchase(slicerId, productTypeId, buyer, 1); + } + + function testRevert_ConfigureProductType_InvalidNFT() public { + NFTGate[] memory gates1 = new NFTGate[](1); + gates1[0] = NFTGate(address(new TestERC20()), NftType.ERC721, 1, 1); + NFTGates memory nftGates = NFTGates(gates1, 1); + + vm.startPrank(productOwner); + + vm.expectRevert(NFTGated.InvalidNFT.selector); + nftGated.configureProductType(slicerId, productTypeId, encodeNFTGates(nftGates)); + + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////// + SLICER TESTS + //////////////////////////////////////////////////////////////*/ + + function testConfigureSlicer() public { + NFTGates[] memory nftGates = generateNFTGates(); + + bytes memory params = encodeNFTGates(nftGates[2]); + + vm.expectEmit(true, true, true, true, address(nftGated)); + emit HookRegistry.SlicerConfigured(slicerId, params); + + vm.prank(productOwner); + nftGated.configureSlicer(slicerId, params); + + assertEq(nftGated.slicerMinOwned(slicerId), 1); + assertEq(nftGated.slicerGates(slicerId)[0].nft, address(nft721)); + assertEq(nftGated.slicerGates(slicerId)[1].nft, address(nft1155)); + assertEq(nftGated.slicerGates(slicerId).length, 2); + + // Buyer doesn't have NFTs yet + ProductLineItem[] memory productLineItems = new ProductLineItem[](1); + productLineItems[0] = + ProductLineItem({productId: uint32(productIds[0]), variantId: uint32(variantId), quantity: uint32(5)}); + assertFalse(nftGated.isSlicerPurchaseAllowed(slicerId, buyer, productLineItems)); + + // Buyer has only ERC721 + nft721.mint(buyer); + assertTrue(nftGated.isSlicerPurchaseAllowed(slicerId, buyer, productLineItems)); + + // Buyer2 has only ERC1155 + nft1155.mint(buyer2); + assertTrue(nftGated.isSlicerPurchaseAllowed(slicerId, buyer2, productLineItems)); + } + + function testConfigureSlicer_AccessControl() public { + vm.expectRevert(abi.encodeWithSelector(SliceContext.NotAuthorized.selector, bytes32(uint256(1 << 1 | 1 << 3)))); + + nftGated.configureSlicer(slicerId, ""); + } + + function testConfigureSlicerSchema() public view { + string memory schema = nftGated.configureSlicerSchema(); + + assertEq(schema, nftGated.configureProductSchema()); + assertTrue(bytes(schema).length > 0); + } + + function testIsSlicerPurchaseAllowed_NoGates() public { + vm.prank(productOwner); + NFTGates memory emptyGates = NFTGates(new NFTGate[](0), 0); + nftGated.configureSlicer(slicerId, encodeNFTGates(emptyGates)); + + ProductLineItem[] memory productLineItems = new ProductLineItem[](1); + productLineItems[0] = + ProductLineItem({productId: uint32(productIds[0]), variantId: uint32(variantId), quantity: uint32(5)}); + assertTrue(nftGated.isSlicerPurchaseAllowed(slicerId, buyer3, productLineItems)); + } + + function testConfigureSlicer__Edit_Remove() public { + NFTGates[] memory nftGates = generateNFTGates(); + + vm.prank(productOwner); + nftGated.configureSlicer(slicerId, encodeNFTGates(nftGates[2])); + + nft721.mint(buyer); + + ProductLineItem[] memory productLineItems = new ProductLineItem[](1); + productLineItems[0] = + ProductLineItem({productId: uint32(productIds[0]), variantId: uint32(variantId), quantity: uint32(5)}); + assertTrue(nftGated.isSlicerPurchaseAllowed(slicerId, buyer, productLineItems)); + + // Remove gates + vm.prank(productOwner); + NFTGates memory emptyGates = NFTGates(new NFTGate[](0), 0); + nftGated.configureSlicer(slicerId, encodeNFTGates(emptyGates)); + + assertTrue(nftGated.isSlicerPurchaseAllowed(slicerId, buyer, productLineItems)); + assertEq(nftGated.slicerGates(slicerId).length, 0); + } + + function testOnSlicerPurchase() public { + NFTGates[] memory nftGates = generateNFTGates(); + + vm.prank(productOwner); + nftGated.configureSlicer(slicerId, encodeNFTGates(nftGates[0])); + + ProductLineItem[] memory productLineItems = new ProductLineItem[](1); + productLineItems[0] = + ProductLineItem({productId: uint32(productIds[0]), variantId: uint32(variantId), quantity: uint32(5)}); + + // Buyer doesn't have NFT - should revert + vm.expectRevert(); + vm.prank(address(PRODUCTS_MODULE)); + nftGated.onSlicerPurchase(slicerId, buyer, productLineItems); + + // Buyer has NFT - should succeed + nft721.mint(buyer); + vm.prank(address(PRODUCTS_MODULE)); + nftGated.onSlicerPurchase(slicerId, buyer, productLineItems); + } + + function testRevert_OnSlicerPurchase_NotPurchase() public { + NFTGates[] memory nftGates = generateNFTGates(); + + vm.prank(productOwner); + nftGated.configureSlicer(slicerId, encodeNFTGates(nftGates[0])); + + nft721.mint(buyer); + + ProductLineItem[] memory productLineItems = new ProductLineItem[](1); + productLineItems[0] = + ProductLineItem({productId: uint32(productIds[0]), variantId: uint32(variantId), quantity: uint32(5)}); + + vm.expectRevert(); + nftGated.onSlicerPurchase(slicerId, buyer, productLineItems); + } + + function testRevert_ConfigureSlicer_InvalidNFT() public { + NFTGate[] memory gates1 = new NFTGate[](1); + gates1[0] = NFTGate(address(new TestERC20()), NftType.ERC721, 1, 1); + NFTGates memory nftGates = NFTGates(gates1, 1); + + vm.startPrank(productOwner); + + vm.expectRevert(NFTGated.InvalidNFT.selector); + nftGated.configureSlicer(slicerId, encodeNFTGates(nftGates)); + + vm.stopPrank(); } /*////////////////////////////////////////////////////////////// @@ -117,4 +407,16 @@ contract NFTGatedTest is RegistryProductActionTest { gates4[1] = gate1155; nftGates[3] = NFTGates(gates4, 2); } + + function encodeNFTGates(NFTGates memory nftGates_) internal pure returns (bytes memory) { + return abi.encode(nftGates_.gates, nftGates_.minOwned); + } +} + +contract TestERC20 is ERC20 { + constructor() ERC20("Test", "TST") {} + + function supportsInterface(bytes4) public pure returns (bool) { + return false; + } } diff --git a/test/actions/NFTGated/mocks/MockNFTGated.sol b/test/actions/NFTGated/mocks/MockNFTGated.sol index d246076..2a2a89f 100644 --- a/test/actions/NFTGated/mocks/MockNFTGated.sol +++ b/test/actions/NFTGated/mocks/MockNFTGated.sol @@ -6,7 +6,23 @@ import {IProductsModule, NFTGated, NFTGate} from "@/hooks/actions/NFTGated/NFTGa contract MockNFTGated is NFTGated { constructor(IProductsModule productsModuleAddress) NFTGated(productsModuleAddress) {} - function gates(uint256 slicerId, uint256 productId) public view returns (NFTGate[] memory) { - return nftGates[slicerId][productId].gates; + function gates(uint256 slicerId, uint256 productId, uint256 variantId) public view returns (NFTGate[] memory) { + return nftGates[slicerId][productId][variantId].gates; + } + + function productTypeGates(uint256 slicerId, uint256 productTypeId) public view returns (NFTGate[] memory) { + return productTypeNftGates[slicerId][productTypeId].gates; + } + + function slicerGates(uint256 slicerId) public view returns (NFTGate[] memory) { + return slicerNftGates[slicerId].gates; + } + + function productTypeMinOwned(uint256 slicerId, uint256 productTypeId) public view returns (uint256) { + return productTypeNftGates[slicerId][productTypeId].minOwned; + } + + function slicerMinOwned(uint256 slicerId) public view returns (uint256) { + return slicerNftGates[slicerId].minOwned; } } diff --git a/test/pricing/TieredDiscount/NFTDiscount.t.sol b/test/pricing/TieredDiscount/NFTDiscount.t.sol index 0aaab39..520954a 100644 --- a/test/pricing/TieredDiscount/NFTDiscount.t.sol +++ b/test/pricing/TieredDiscount/NFTDiscount.t.sol @@ -2,19 +2,23 @@ pragma solidity ^0.8.30; import {RegistryProductPriceTest} from "@test/utils/RegistryProductPriceTest.sol"; -import {console2} from "forge-std/console2.sol"; import { IProductsModule, + HookRegistry, NFTDiscount, DiscountParams, NFTType } from "@/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol"; +import {ProductLineItem} from "@/hooks/pricing/TieredDiscount/TieredDiscount.sol"; +import {SliceContext} from "slice/utils/SliceContext.sol"; import {MockERC721} from "@test/utils/mocks/MockERC721.sol"; import {MockERC1155} from "@test/utils/mocks/MockERC1155.sol"; address constant USDC = address(1); uint256 constant slicerId = 0; uint256 constant productId = 1; +uint256 constant variantId = 0; +uint256 constant productTypeId = 2; uint80 constant percentDiscountOne = 1000; // 10% uint80 constant percentDiscountTwo = 2000; // 20% @@ -25,7 +29,7 @@ contract NFTDiscountTest is RegistryProductPriceTest { MockERC721 nftThree = new MockERC721(); MockERC1155 nft1155 = new MockERC1155(); - uint256 quantity = 1; + uint256 quantity = 5; uint8 minNftQuantity = 1; function setUp() public { @@ -35,6 +39,221 @@ contract NFTDiscountTest is RegistryProductPriceTest { nftOne.mint(buyer); } + function testConfigureProductType() public { + DiscountParams[] memory discountParams = new DiscountParams[](2); + + discountParams[0] = DiscountParams({ + nft: address(nftTwo), + discount: percentDiscountTwo, + minQuantity: minNftQuantity, + nftType: NFTType.ERC721, + tokenId: 0 + }); + discountParams[1] = DiscountParams({ + nft: address(nftOne), + discount: percentDiscountOne, + minQuantity: minNftQuantity, + nftType: NFTType.ERC721, + tokenId: 0 + }); + + bytes memory params = abi.encode(discountParams); + + vm.expectEmit(true, true, true, true, address(erc721GatedDiscount)); + emit HookRegistry.ProductTypeConfigured(slicerId, productTypeId, params); + + vm.prank(productOwner); + erc721GatedDiscount.configureProductType(slicerId, productTypeId, params); + + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); + + uint256 price = erc721GatedDiscount.productTypePrice(slicerId, productTypeId, ETH, basePrice, quantity, buyer); + + assertEq(price, (basePrice - (basePrice * percentDiscountOne) / 1e4)); + + nftTwo.mint(buyer); + + uint256 priceWithHigherDiscount = + erc721GatedDiscount.productTypePrice(slicerId, productTypeId, ETH, basePrice, quantity, buyer); + + assertEq(priceWithHigherDiscount, (basePrice - (basePrice * percentDiscountTwo) / 1e4)); + } + + function testConfigureProductType_AccessControl() public { + vm.expectRevert(abi.encodeWithSelector(SliceContext.NotAuthorized.selector, bytes32(uint256(1 << 1 | 1 << 3)))); + + erc721GatedDiscount.configureProductType(slicerId, productTypeId, ""); + } + + function testConfigureProductTypeSchema() public view { + string memory schema = erc721GatedDiscount.configureProductTypeSchema(); + + assertEq(schema, erc721GatedDiscount.configureProductSchema()); + assertTrue(bytes(schema).length > 0); + } + + function testProductTypePrice_NoDiscount() public { + vm.prank(productOwner); + erc721GatedDiscount.configureProductType(slicerId, productTypeId, abi.encode(new DiscountParams[](0))); + + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); + uint256 price = erc721GatedDiscount.productTypePrice(slicerId, productTypeId, ETH, basePrice, quantity, buyer2); + + assertEq(price, basePrice); + } + + function testConfigureProductType__Edit_Remove() public { + DiscountParams[] memory discountParams = new DiscountParams[](2); + + discountParams[0] = DiscountParams({ + nft: address(nftTwo), + discount: percentDiscountTwo, + minQuantity: minNftQuantity, + nftType: NFTType.ERC721, + tokenId: 0 + }); + discountParams[1] = DiscountParams({ + nft: address(nftOne), + discount: percentDiscountOne, + minQuantity: minNftQuantity, + nftType: NFTType.ERC721, + tokenId: 0 + }); + + vm.prank(productOwner); + erc721GatedDiscount.configureProductType(slicerId, productTypeId, abi.encode(discountParams)); + + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); + + uint256 price = erc721GatedDiscount.productTypePrice(slicerId, productTypeId, ETH, basePrice, quantity, buyer); + + assertEq(price, (basePrice - (basePrice * percentDiscountOne) / 1e4)); + + vm.prank(productOwner); + erc721GatedDiscount.configureProductType(slicerId, productTypeId, abi.encode(new DiscountParams[](0))); + + uint256 resetPrice = + erc721GatedDiscount.productTypePrice(slicerId, productTypeId, ETH, basePrice, quantity, buyer); + + assertEq(resetPrice, basePrice); + } + + function testConfigureSlicer() public { + DiscountParams[] memory discountParams = new DiscountParams[](2); + + discountParams[0] = DiscountParams({ + nft: address(nftTwo), + discount: percentDiscountTwo, + minQuantity: minNftQuantity, + nftType: NFTType.ERC721, + tokenId: 0 + }); + discountParams[1] = DiscountParams({ + nft: address(nftOne), + discount: percentDiscountOne, + minQuantity: minNftQuantity, + nftType: NFTType.ERC721, + tokenId: 0 + }); + + bytes memory params = abi.encode(discountParams); + + vm.expectEmit(true, true, true, true, address(erc721GatedDiscount)); + emit HookRegistry.SlicerConfigured(slicerId, params); + + vm.prank(productOwner); + erc721GatedDiscount.configureSlicer(slicerId, params); + + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); + + ProductLineItem[] memory productLineItems = new ProductLineItem[](1); + productLineItems[0] = + ProductLineItem({productId: uint32(productId), variantId: uint32(variantId), quantity: uint32(quantity)}); + uint256[] memory basePrices = new uint256[](1); + basePrices[0] = basePrice; + uint256[] memory newBasePrices = + erc721GatedDiscount.slicerProductPrice(slicerId, ETH, buyer, productLineItems, basePrices); + + assertEq(newBasePrices[0], (basePrice - (basePrice * percentDiscountOne) / 1e4)); + + nftTwo.mint(buyer); + + newBasePrices = erc721GatedDiscount.slicerProductPrice(slicerId, ETH, buyer, productLineItems, basePrices); + + assertEq(newBasePrices[0], (basePrice - (basePrice * percentDiscountTwo) / 1e4)); + } + + function testConfigureSlicer_AccessControl() public { + vm.expectRevert(abi.encodeWithSelector(SliceContext.NotAuthorized.selector, bytes32(uint256(1 << 1 | 1 << 3)))); + + erc721GatedDiscount.configureSlicer(slicerId, ""); + } + + function testConfigureSlicerSchema() public view { + string memory schema = erc721GatedDiscount.configureSlicerSchema(); + + assertEq(schema, erc721GatedDiscount.configureProductSchema()); + assertTrue(bytes(schema).length > 0); + } + + function testSlicerProductPrice_NoDiscount() public { + vm.prank(productOwner); + erc721GatedDiscount.configureSlicer(slicerId, abi.encode(new DiscountParams[](0))); + + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); + + ProductLineItem[] memory productLineItems = new ProductLineItem[](1); + productLineItems[0] = + ProductLineItem({productId: uint32(productId), variantId: uint32(variantId), quantity: uint32(quantity)}); + uint256[] memory basePrices = new uint256[](1); + basePrices[0] = basePrice; + uint256[] memory newBasePrices = + erc721GatedDiscount.slicerProductPrice(slicerId, ETH, buyer3, productLineItems, basePrices); + + assertEq(newBasePrices[0], basePrice); + } + + function testConfigureSlicer__Edit_Remove() public { + DiscountParams[] memory discountParams = new DiscountParams[](2); + + discountParams[0] = DiscountParams({ + nft: address(nftTwo), + discount: percentDiscountTwo, + minQuantity: minNftQuantity, + nftType: NFTType.ERC721, + tokenId: 0 + }); + discountParams[1] = DiscountParams({ + nft: address(nftOne), + discount: percentDiscountOne, + minQuantity: minNftQuantity, + nftType: NFTType.ERC721, + tokenId: 0 + }); + + vm.prank(productOwner); + erc721GatedDiscount.configureSlicer(slicerId, abi.encode(discountParams)); + + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); + + ProductLineItem[] memory productLineItems = new ProductLineItem[](1); + productLineItems[0] = + ProductLineItem({productId: uint32(productId), variantId: uint32(variantId), quantity: uint32(quantity)}); + uint256[] memory basePrices = new uint256[](1); + basePrices[0] = basePrice; + uint256[] memory newBasePrices = + erc721GatedDiscount.slicerProductPrice(slicerId, ETH, buyer, productLineItems, basePrices); + + assertEq(newBasePrices[0], (basePrice - (basePrice * percentDiscountOne) / 1e4)); + + vm.prank(productOwner); + erc721GatedDiscount.configureSlicer(slicerId, abi.encode(new DiscountParams[](0))); + + newBasePrices = erc721GatedDiscount.slicerProductPrice(slicerId, ETH, buyer, productLineItems, basePrices); + + assertEq(newBasePrices[0], basePrice); + } + function testConfigureProduct__ETH() public { DiscountParams[] memory discountParams = new DiscountParams[](1); @@ -48,16 +267,15 @@ contract NFTDiscountTest is RegistryProductPriceTest { }); vm.prank(productOwner); - erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + erc721GatedDiscount.configureProduct(slicerId, productId, variantId, abi.encode(discountParams)); - /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + /// check product price + uint256 price = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); - assertEq(currencyPrice, 0); + assertEq(price, (basePrice - (basePrice * percentDiscountOne) / 1e4)); } function testConfigureProduct__ERC20() public { @@ -73,16 +291,15 @@ contract NFTDiscountTest is RegistryProductPriceTest { }); vm.prank(productOwner); - erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + erc721GatedDiscount.configureProduct(slicerId, productId, variantId, abi.encode(discountParams)); - /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, USDC, quantity, buyer, ""); + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, USDC, quantity); - (, uint256 baseCurrencyPrice) = PRODUCTS_MODULE.basePrice(slicerId, productId, USDC, quantity); + /// check product price + uint256 price = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, USDC, basePrice, quantity, buyer, ""); - assertEq(currencyPrice, quantity * (baseCurrencyPrice - (baseCurrencyPrice * percentDiscountOne) / 1e4)); - assertTrue(ethPrice == 0); + assertEq(price, (basePrice - (basePrice * percentDiscountOne) / 1e4)); } function testConfigureProduct__ERC1155() public { @@ -98,23 +315,21 @@ contract NFTDiscountTest is RegistryProductPriceTest { }); vm.prank(productOwner); - erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + erc721GatedDiscount.configureProduct(slicerId, productId, variantId, abi.encode(discountParams)); - /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, USDC, quantity, buyer, ""); + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, USDC, quantity); - (, uint256 baseCurrencyPrice) = PRODUCTS_MODULE.basePrice(slicerId, productId, USDC, quantity); + /// check product price + uint256 price = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, USDC, basePrice, quantity, buyer, ""); - assertEq(currencyPrice, quantity * baseCurrencyPrice); - assertEq(ethPrice, 0); + assertEq(price, basePrice); nft1155.mint(buyer); - (ethPrice, currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, USDC, quantity, buyer, ""); + price = erc721GatedDiscount.productPrice(slicerId, productId, variantId, USDC, basePrice, quantity, buyer, ""); - assertEq(currencyPrice, quantity * (baseCurrencyPrice - (baseCurrencyPrice * percentDiscountOne) / 1e4)); - assertEq(ethPrice, 0); + assertEq(price, (basePrice - (basePrice * percentDiscountOne) / 1e4)); } function testConfigureProduct__HigherDiscount() public { @@ -136,23 +351,21 @@ contract NFTDiscountTest is RegistryProductPriceTest { }); vm.prank(productOwner); - erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + erc721GatedDiscount.configureProduct(slicerId, productId, variantId, abi.encode(discountParams)); - /// check product price for ETH - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + /// check product price for ETH + uint256 price = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); - assertEq(currencyPrice, 0); + assertEq(price, (basePrice - (basePrice * percentDiscountOne) / 1e4)); nft1155.mint(buyer); - (ethPrice, currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + price = erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountTwo) / 1e4)); - assertEq(currencyPrice, 0); + assertEq(price, (basePrice - (basePrice * percentDiscountTwo) / 1e4)); } function testRevert_ProductPrice__NotNFTOwner() public { @@ -168,16 +381,15 @@ contract NFTDiscountTest is RegistryProductPriceTest { }); vm.prank(productOwner); - erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + erc721GatedDiscount.configureProduct(slicerId, productId, variantId, abi.encode(discountParams)); - /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + /// check product price + uint256 price = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(ethPrice, quantity * baseEthPrice); - assertEq(currencyPrice, 0); + assertEq(price, basePrice); } function testProductPrice__MinQuantity() public { @@ -193,26 +405,24 @@ contract NFTDiscountTest is RegistryProductPriceTest { }); vm.prank(productOwner); - erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + erc721GatedDiscount.configureProduct(slicerId, productId, variantId, abi.encode(discountParams)); - /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + /// check product price + uint256 price = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(ethPrice, quantity * baseEthPrice); - assertEq(currencyPrice, 0); + assertEq(price, basePrice); /// Buyer owns 2 NFTs, minQuantity is 2 nftOne.mint(buyer); /// check product price - (uint256 secondEthPrice, uint256 secondCurrencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + uint256 secondPrice = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(secondEthPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); - assertEq(secondCurrencyPrice, 0); + assertEq(secondPrice, (basePrice - (basePrice * percentDiscountOne) / 1e4)); } function testProductPrice__MultipleBoughtQuantity() public { @@ -227,19 +437,18 @@ contract NFTDiscountTest is RegistryProductPriceTest { }); vm.prank(productOwner); - erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + erc721GatedDiscount.configureProduct(slicerId, productId, variantId, abi.encode(discountParams)); // buy multiple products quantity = 6; - /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + /// check product price + uint256 price = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); - assertEq(currencyPrice, 0); + assertEq(price, (basePrice - (basePrice * percentDiscountOne) / 1e4)); } function testConfigureProduct__Edit_Add() public { @@ -254,19 +463,18 @@ contract NFTDiscountTest is RegistryProductPriceTest { }); vm.prank(productOwner); - erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + erc721GatedDiscount.configureProduct(slicerId, productId, variantId, abi.encode(discountParams)); // mint NFT 2 nftTwo.mint(buyer); - /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + /// check product price + uint256 price = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountTwo) / 1e4)); - assertEq(currencyPrice, 0); + assertEq(price, (basePrice - (basePrice * percentDiscountTwo) / 1e4)); discountParams = new DiscountParams[](2); @@ -288,14 +496,13 @@ contract NFTDiscountTest is RegistryProductPriceTest { }); vm.prank(productOwner); - erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + erc721GatedDiscount.configureProduct(slicerId, productId, variantId, abi.encode(discountParams)); /// check product price - (uint256 secondEthPrice, uint256 secondCurrencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + uint256 secondPrice = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(secondEthPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); - assertEq(secondCurrencyPrice, 0); + assertEq(secondPrice, (basePrice - (basePrice * percentDiscountOne) / 1e4)); } function testConfigureProduct__Edit_Remove() public { @@ -322,27 +529,25 @@ contract NFTDiscountTest is RegistryProductPriceTest { }); vm.prank(productOwner); - erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + erc721GatedDiscount.configureProduct(slicerId, productId, variantId, abi.encode(discountParams)); - /// check product price - (uint256 secondEthPrice, uint256 secondCurrencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + /// check product price + uint256 secondPrice = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(secondEthPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); - assertEq(secondCurrencyPrice, 0); + assertEq(secondPrice, (basePrice - (basePrice * percentDiscountOne) / 1e4)); discountParams = new DiscountParams[](0); vm.prank(productOwner); - erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + erc721GatedDiscount.configureProduct(slicerId, productId, variantId, abi.encode(discountParams)); /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + uint256 price = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(ethPrice, quantity * baseEthPrice); - assertEq(currencyPrice, 0); + assertEq(price, basePrice); } } diff --git a/test/pricing/VRGDA/LinearVRGDA.t.sol b/test/pricing/VRGDA/LinearVRGDA.t.sol index 1b57588..905051a 100644 --- a/test/pricing/VRGDA/LinearVRGDA.t.sol +++ b/test/pricing/VRGDA/LinearVRGDA.t.sol @@ -13,26 +13,29 @@ uint256 constant MAX_SELLABLE = 6392; uint256 constant slicerId = 0; uint256 constant productId = 1; -int128 constant targetPriceConstant = 69.42e18; -uint128 constant min = 1e18; -int256 constant priceDecayPercent = 0.31e18; +uint256 constant variantId = 0; +uint16 constant min = 1e4; +int184 constant priceDecayPercent = 0.31e18; int256 constant perTimeUnit = 2e18; contract LinearVRGDATest is RegistryProductPriceTest { MockLinearVRGDAPrices vrgda; + int128 targetPriceConstant = + int128(int256(PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, address(0), 1))); + int128 targetPriceConstant_Erc20 = + int128(int256(PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, address(20), 1))); + + uint256 minAmount = uint256(int256(targetPriceConstant)) * min / 1e5; + uint256 minAmount_Erc20 = uint256(int256(targetPriceConstant_Erc20)) * min / 1e5; function setUp() public { vrgda = new MockLinearVRGDAPrices(PRODUCTS_MODULE); _setHook(address(vrgda)); - LinearVRGDAParams[] memory linearParams = new LinearVRGDAParams[](2); - linearParams[0] = LinearVRGDAParams(address(0), targetPriceConstant, min, perTimeUnit); - linearParams[1] = LinearVRGDAParams(address(20), targetPriceConstant, min, perTimeUnit); - - bytes memory params = abi.encode(linearParams, priceDecayPercent); + bytes memory params = abi.encode(priceDecayPercent, min, perTimeUnit); vm.startPrank(productOwner); - vrgda.configureProduct(slicerId, productId, params); + vrgda.configureProduct(slicerId, productId, variantId, params); vm.stopPrank(); } @@ -42,7 +45,7 @@ contract LinearVRGDATest is RegistryProductPriceTest { int256 decayConstant = wadLn(1e18 - priceDecayPercent); uint256 cost = vrgda.getVRGDAPrice( - targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), 0, perTimeUnit, min + targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), 0, perTimeUnit, minAmount ); assertApproxEqAbs(cost, uint256(uint128(targetPriceConstant)), 5e14); } @@ -56,7 +59,7 @@ contract LinearVRGDATest is RegistryProductPriceTest { int256 decayConstant = wadLn(1e18 - priceDecayPercent); uint256 cost = vrgda.getVRGDAPrice( - targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), numMint, perTimeUnit, min + targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), numMint, perTimeUnit, minAmount ); assertApproxEqAbs(cost, uint256(uint128(targetPriceConstant)), 5e14); } @@ -71,9 +74,9 @@ contract LinearVRGDATest is RegistryProductPriceTest { int256 decayConstant = wadLn(1e18 - priceDecayPercent); uint256 cost = vrgda.getVRGDAPrice( - targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), numMint, perTimeUnit, min + targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), numMint, perTimeUnit, minAmount ); - assertEq(cost, min); + assertEq(cost, minAmount); } function testPricingAdjustedByQuantity() public { @@ -86,16 +89,16 @@ contract LinearVRGDATest is RegistryProductPriceTest { int256 decayConstant = wadLn(1e18 - priceDecayPercent); uint256 costProduct1 = vrgda.getAdjustedVRGDAPrice( - targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), numMint, perTimeUnit, min, 1 + targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), numMint, perTimeUnit, minAmount, 1 ); uint256 costProduct2 = vrgda.getAdjustedVRGDAPrice( - targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), numMint + 1, perTimeUnit, min, 1 + targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), numMint + 1, perTimeUnit, minAmount, 1 ); uint256 costProduct3 = vrgda.getAdjustedVRGDAPrice( - targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), numMint + 2, perTimeUnit, min, 1 + targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), numMint + 2, perTimeUnit, minAmount, 1 ); uint256 costMultiple = vrgda.getAdjustedVRGDAPrice( - targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), numMint, perTimeUnit, min, 3 + targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), numMint, perTimeUnit, minAmount, 3 ); assertApproxEqAbs(costMultiple, uint256(costProduct1 + costProduct2 + costProduct3), 0.00001e18); @@ -112,7 +115,7 @@ contract LinearVRGDATest is RegistryProductPriceTest { vrgda.getTargetSaleTime(toWadUnsafe(sold + 1), perTimeUnit), sold, perTimeUnit, - min + minAmount ), uint256(uint128(targetPriceConstant)), 0.00001e18 @@ -121,14 +124,11 @@ contract LinearVRGDATest is RegistryProductPriceTest { function testSetMultiplePrices() public { uint256 productId_ = 2; - LinearVRGDAParams[] memory linearParams = new LinearVRGDAParams[](2); - linearParams[0] = LinearVRGDAParams(address(0), targetPriceConstant, min, perTimeUnit); - linearParams[1] = LinearVRGDAParams(address(20), targetPriceConstant, min, perTimeUnit); - bytes memory params = abi.encode(linearParams, priceDecayPercent); + bytes memory params = abi.encode(priceDecayPercent, min, perTimeUnit); vm.startPrank(productOwner); - vrgda.configureProduct(slicerId, productId_, params); + vrgda.configureProduct(slicerId, productId_, variantId, params); vm.stopPrank(); // Our VRGDA targets this number of mints at given time. @@ -136,17 +136,13 @@ contract LinearVRGDATest is RegistryProductPriceTest { vm.warp(block.timestamp + timeDelta); - (uint256 ethPrice, uint256 currencyPrice) = - vrgda.productPrice(slicerId, productId_, address(0), 1, address(0), ""); + uint256 price = vrgda.productPrice(slicerId, productId_, variantId, address(0), 0, 1, address(0), ""); - assertApproxEqAbs(uint256(uint128(targetPriceConstant)), ethPrice, 0.00001e18); - assertEq(currencyPrice, 0); + assertApproxEqAbs(uint256(uint128(targetPriceConstant)), price, 0.00001e18); - (uint256 ethPrice2, uint256 currencyPrice2) = - vrgda.productPrice(slicerId, productId_, address(20), 1, address(0), ""); + price = vrgda.productPrice(slicerId, productId_, variantId, address(20), 0, 1, address(0), ""); - assertEq(ethPrice2, 0); - assertApproxEqAbs(uint256(uint128(targetPriceConstant)), currencyPrice2, 0.00001e18); + assertApproxEqAbs(uint256(uint128(targetPriceConstant_Erc20)), price, 0.00001e18); } function testProductPriceEth() public { @@ -160,14 +156,12 @@ contract LinearVRGDATest is RegistryProductPriceTest { int256 decayConstant = wadLn(1e18 - priceDecayPercent); uint256 cost = vrgda.getAdjustedVRGDAPrice( - targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), 0, perTimeUnit, min, 1 + targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), 0, perTimeUnit, minAmount, 1 ); - (uint256 ethPrice, uint256 currencyPrice) = - vrgda.productPrice(slicerId, productId, ethCurrency, 1, address(0), ""); + uint256 price = vrgda.productPrice(slicerId, productId, variantId, ethCurrency, 0, 1, address(0), ""); - assertApproxEqAbs(cost, ethPrice, 0.00001e18); - assertEq(currencyPrice, 0); + assertApproxEqAbs(cost, price, 0.00001e18); } function testProductPriceErc20() public { @@ -181,14 +175,18 @@ contract LinearVRGDATest is RegistryProductPriceTest { int256 decayConstant = wadLn(1e18 - priceDecayPercent); uint256 cost = vrgda.getAdjustedVRGDAPrice( - targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), 0, perTimeUnit, min, 1 + targetPriceConstant_Erc20, + decayConstant, + toDaysWadUnsafe(block.timestamp), + 0, + perTimeUnit, + minAmount_Erc20, + 1 ); - (uint256 ethPrice, uint256 currencyPrice) = - vrgda.productPrice(slicerId, productId, erc20Currency, 1, address(0), ""); + uint256 price = vrgda.productPrice(slicerId, productId, variantId, erc20Currency, 0, 1, address(0), ""); - assertApproxEqAbs(cost, currencyPrice, 0.00001e18); - assertEq(ethPrice, 0); + assertApproxEqAbs(cost, price, 0.00001e18); } function testProductPriceMultiple() public { @@ -202,10 +200,10 @@ contract LinearVRGDATest is RegistryProductPriceTest { int256 decayConstant = wadLn(1e18 - priceDecayPercent); uint256 costMultiple = vrgda.getAdjustedVRGDAPrice( - targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), 0, perTimeUnit, min, 3 + targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), 0, perTimeUnit, minAmount, 3 ); - (uint256 ethPrice,) = vrgda.productPrice(slicerId, productId, ethCurrency, 3, address(0), ""); + uint256 price = vrgda.productPrice(slicerId, productId, variantId, ethCurrency, 0, 3, address(0), ""); - assertApproxEqAbs(costMultiple, ethPrice, 5e14); + assertApproxEqAbs(costMultiple, price, 5e14); } } diff --git a/test/pricing/VRGDA/LogisticVRGDA.t.sol b/test/pricing/VRGDA/LogisticVRGDA.t.sol index 654e3af..76e9642 100644 --- a/test/pricing/VRGDA/LogisticVRGDA.t.sol +++ b/test/pricing/VRGDA/LogisticVRGDA.t.sol @@ -3,9 +3,7 @@ pragma solidity ^0.8.30; import {RegistryProductPriceTest} from "@test/utils/RegistryProductPriceTest.sol"; import {unsafeDiv, wadLn, toWadUnsafe, toDaysWadUnsafe, fromDaysWadUnsafe} from "@/utils/math/SignedWadMath.sol"; - import "./mocks/MockLogisticVRGDAPrices.sol"; -import "forge-std/console2.sol"; uint256 constant ONE_THOUSAND_YEARS = 356 days * 1000; @@ -13,28 +11,31 @@ uint256 constant MAX_SELLABLE = 6392; uint256 constant slicerId = 0; uint256 constant productId = 1; -int128 constant targetPriceConstant = 69.42e18; -uint128 constant min = 1e18; -int256 constant priceDecayPercent = 0.31e18; +uint256 constant variantId = 0; +uint16 constant min = 1e4; +int184 constant priceDecayPercent = 0.31e18; int256 constant timeScale = 0.0023e18; int256 constant logisticLimitAdjusted = int256((MAX_SELLABLE + 1) * 2e18); int256 constant logisticLimitDoubled = int256((MAX_SELLABLE + 1e18) * 2e18); contract LogisticVRGDATest is RegistryProductPriceTest { MockLogisticVRGDAPrices vrgda; + int128 targetPriceConstant = + int128(int256(PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, address(0), 1))); + int128 targetPriceConstant_Erc20 = + int128(int256(PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, address(20), 1))); + + uint256 minAmount = uint256(int256(targetPriceConstant)) * min / 1e5; + uint256 minAmount_Erc20 = uint256(int256(targetPriceConstant_Erc20)) * min / 1e5; function setUp() public { vrgda = new MockLogisticVRGDAPrices(PRODUCTS_MODULE); _setHook(address(vrgda)); - LogisticVRGDAParams[] memory logisticParams = new LogisticVRGDAParams[](2); - logisticParams[0] = LogisticVRGDAParams(address(0), targetPriceConstant, min, timeScale); - logisticParams[1] = LogisticVRGDAParams(address(20), targetPriceConstant, min, timeScale); - - bytes memory params = abi.encode(logisticParams, priceDecayPercent); + bytes memory params = abi.encode(priceDecayPercent, min, timeScale); vm.startPrank(productOwner); - vrgda.configureProduct(slicerId, productId, params); + vrgda.configureProduct(slicerId, productId, variantId, params); vm.stopPrank(); } @@ -55,7 +56,7 @@ contract LogisticVRGDATest is RegistryProductPriceTest { logisticLimit * 2e18, sold, timeScale, - min + minAmount ); assertApproxEqAbs(cost, uint256(uint128(targetPriceConstant)), 1e13); } @@ -77,7 +78,7 @@ contract LogisticVRGDATest is RegistryProductPriceTest { logisticLimit * 2e18, numMint, timeScale, - min + minAmount ); // Equal within 2 percent since num mint is rounded from true decimal amount. @@ -101,9 +102,9 @@ contract LogisticVRGDATest is RegistryProductPriceTest { logisticLimit * 2e18, numMint, timeScale, - min + minAmount ); - assertEq(cost, min); + assertEq(cost, minAmount); } function testPricingAdjustedByQuantity() public { @@ -121,7 +122,7 @@ contract LogisticVRGDATest is RegistryProductPriceTest { toWadUnsafe(MAX_SELLABLE + 1), numMint, timeScale, - min, + minAmount, 1 ); uint256 costProduct2 = vrgda.getAdjustedVRGDALogisticPrice( @@ -131,7 +132,7 @@ contract LogisticVRGDATest is RegistryProductPriceTest { toWadUnsafe(MAX_SELLABLE + 1), numMint + 1, timeScale, - min, + minAmount, 1 ); uint256 costProduct3 = vrgda.getAdjustedVRGDALogisticPrice( @@ -141,7 +142,7 @@ contract LogisticVRGDATest is RegistryProductPriceTest { toWadUnsafe(MAX_SELLABLE + 1), numMint + 2, timeScale, - min, + minAmount, 1 ); uint256 costMultiple = vrgda.getAdjustedVRGDALogisticPrice( @@ -151,7 +152,7 @@ contract LogisticVRGDATest is RegistryProductPriceTest { toWadUnsafe(MAX_SELLABLE + 1), numMint, timeScale, - min, + minAmount, 3 ); @@ -161,30 +162,23 @@ contract LogisticVRGDATest is RegistryProductPriceTest { function testSetMultiplePrices() public { // uint256 targetPriceTest = 7.3013e18; uint256 productIdTest = 2; - LogisticVRGDAParams[] memory logisticParams = new LogisticVRGDAParams[](2); - logisticParams[0] = LogisticVRGDAParams(address(0), targetPriceConstant, min, timeScale); - logisticParams[1] = LogisticVRGDAParams(address(20), targetPriceConstant, min, timeScale); - bytes memory params = abi.encode(logisticParams, priceDecayPercent); + bytes memory params = abi.encode(priceDecayPercent, min, timeScale); vm.startPrank(productOwner); - vrgda.configureProduct(slicerId, productIdTest, params); + vrgda.configureProduct(slicerId, productIdTest, variantId, params); vm.stopPrank(); vm.warp(block.timestamp + 10 days); - (uint256 ethPrice, uint256 currencyPrice) = - vrgda.productPrice(slicerId, productIdTest, address(0), 1, address(0), ""); + uint256 price = vrgda.productPrice(slicerId, productIdTest, variantId, address(0), 0, 1, address(0), ""); // assertApproxEqAbs(uint256(targetPriceTest), ethPrice, 1e18); - assertNotEq(ethPrice, 0); - assertEq(currencyPrice, 0); + assertNotEq(price, 0); - (uint256 ethPrice2, uint256 currencyPrice2) = - vrgda.productPrice(slicerId, productId, address(20), 1, address(0), ""); + uint256 price2 = vrgda.productPrice(slicerId, productId, variantId, address(20), 0, 1, address(0), ""); - assertEq(ethPrice2, 0); - assertNotEq(currencyPrice2, 0); + assertNotEq(price2, 0); } function testProductPriceEth() public { @@ -203,15 +197,13 @@ contract LogisticVRGDATest is RegistryProductPriceTest { toWadUnsafe(MAX_SELLABLE + 1), 0, timeScale, - min, + minAmount, 1 ); - (uint256 ethPrice, uint256 currencyPrice) = - vrgda.productPrice(slicerId, productId, ethCurrency, 1, address(0), ""); + uint256 price = vrgda.productPrice(slicerId, productId, variantId, ethCurrency, 0, 1, address(0), ""); - assertApproxEqAbs(cost, ethPrice, 0.00001e18); - assertEq(currencyPrice, 0); + assertApproxEqAbs(cost, price, 0.00001e18); } function testProductPriceErc20() public { @@ -224,21 +216,19 @@ contract LogisticVRGDATest is RegistryProductPriceTest { vm.warp(block.timestamp + timeDelta); uint256 cost = vrgda.getAdjustedVRGDALogisticPrice( - targetPriceConstant, + targetPriceConstant_Erc20, decayConstant, toDaysWadUnsafe(block.timestamp), toWadUnsafe(MAX_SELLABLE + 1), 0, timeScale, - min, + minAmount_Erc20, 1 ); - (uint256 ethPrice, uint256 currencyPrice) = - vrgda.productPrice(slicerId, productId, erc20Currency, 1, address(0), ""); + uint256 price = vrgda.productPrice(slicerId, productId, variantId, erc20Currency, 0, 1, address(0), ""); - assertApproxEqAbs(cost, currencyPrice, 0.00001e18); - assertEq(ethPrice, 0); + assertApproxEqAbs(cost, price, 0.00001e18); } function testProductPriceMultiple() public { @@ -257,12 +247,12 @@ contract LogisticVRGDATest is RegistryProductPriceTest { toWadUnsafe(MAX_SELLABLE + 1), 0, timeScale, - min, + minAmount, 3 ); - (uint256 ethPrice,) = vrgda.productPrice(slicerId, productId, ethCurrency, 3, address(0), ""); + uint256 price = vrgda.productPrice(slicerId, productId, variantId, ethCurrency, 0, 3, address(0), ""); - assertApproxEqAbs(costMultiple, ethPrice, 5e14); + assertApproxEqAbs(costMultiple, price, 5e14); } function testGetTargetSaleTimeDoesNotRevertEarly() public view { @@ -292,7 +282,7 @@ contract LogisticVRGDATest is RegistryProductPriceTest { logisticLimit * 2e18, bound(sold, 0, 1730), timeScale, - min + minAmount ); } @@ -308,7 +298,7 @@ contract LogisticVRGDATest is RegistryProductPriceTest { logisticLimit * 2e18, bound(sold, 0, 6391), timeScale, - min + minAmount ); } @@ -325,7 +315,7 @@ contract LogisticVRGDATest is RegistryProductPriceTest { logisticLimit * 2e18, bound(sold, 6392, type(uint128).max), timeScale, - min + minAmount ); } @@ -344,7 +334,7 @@ contract LogisticVRGDATest is RegistryProductPriceTest { logisticLimit * 2e18, sold, timeScale, - min + minAmount ), uint256(uint128(targetPriceConstant)), 0.00001e18 diff --git a/test/pricing/VRGDA/correctness/LinearVRGDACorrectness.t.sol b/test/pricing/VRGDA/correctness/LinearVRGDACorrectness.t.sol index 105432f..3f15239 100644 --- a/test/pricing/VRGDA/correctness/LinearVRGDACorrectness.t.sol +++ b/test/pricing/VRGDA/correctness/LinearVRGDACorrectness.t.sol @@ -1,12 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import {console} from "forge-std/console.sol"; import {Vm} from "forge-std/Vm.sol"; import {RegistryProductPriceTest} from "@test/utils/RegistryProductPriceTest.sol"; import {wadLn, toWadUnsafe} from "@/utils/math/SignedWadMath.sol"; import {IProductsModule} from "@/utils/ProductPrice.sol"; -import {MockLinearVRGDAPrices, LinearVRGDAParams} from "../mocks/MockLinearVRGDAPrices.sol"; +import {MockLinearVRGDAPrices} from "../mocks/MockLinearVRGDAPrices.sol"; contract LinearVRGDACorrectnessTest is RegistryProductPriceTest { // Sample parameters for differential fuzzing campaign. @@ -15,9 +14,10 @@ contract LinearVRGDACorrectnessTest is RegistryProductPriceTest { uint256 constant slicerId = 0; uint256 constant productId = 1; + uint256 constant variantId = 0; int128 constant targetPriceConstant = 69.42e18; - uint128 constant min = 1e18; - int256 constant priceDecayPercent = 0.31e18; + uint16 constant min = 1e3; + int184 constant priceDecayPercent = 0.31e18; int256 constant perTimeUnit = 2e18; MockLinearVRGDAPrices vrgda; @@ -26,12 +26,9 @@ contract LinearVRGDACorrectnessTest is RegistryProductPriceTest { vrgda = new MockLinearVRGDAPrices(PRODUCTS_MODULE); _setHook(address(vrgda)); - LinearVRGDAParams[] memory linearParams = new LinearVRGDAParams[](1); - linearParams[0] = LinearVRGDAParams(address(0), targetPriceConstant, min, perTimeUnit); - vm.prank(productOwner); - bytes memory params = abi.encode(linearParams, priceDecayPercent); - vrgda.configureProduct(slicerId, productId, params); + bytes memory params = abi.encode(priceDecayPercent, min, perTimeUnit); + vrgda.configureProduct(slicerId, productId, variantId, params); } function testFFICorrectness() public { @@ -48,9 +45,6 @@ contract LinearVRGDACorrectnessTest is RegistryProductPriceTest { uint256 expectedPrice = calculatePrice(targetPriceConstant, priceDecayPercent, perTimeUnit, timeSinceStart, numSold); - console.log("actual price", actualPrice); - console.log("expected price", expectedPrice); - // Check approximate equality. assertApproxEqAbs(expectedPrice, actualPrice, 0.00001e18); diff --git a/test/pricingActions/FirstForFree/FirstForFree.t.sol b/test/pricingActions/FirstForFree/FirstForFree.t.sol index a4bbfa9..6be2046 100644 --- a/test/pricingActions/FirstForFree/FirstForFree.t.sol +++ b/test/pricingActions/FirstForFree/FirstForFree.t.sol @@ -8,10 +8,6 @@ import {TokenCondition, TokenType} from "@/hooks/pricingActions/FirstForFree/typ import {ITokenERC1155} from "@/hooks/pricingActions/FirstForFree/utils/ITokenERC1155.sol"; import {MockERC721} from "@test/utils/mocks/MockERC721.sol"; -import {console2} from "forge-std/console2.sol"; - -uint256 constant slicerId = 0; - contract MockERC1155Token is ITokenERC1155 { mapping(address => mapping(uint256 => uint256)) public balanceOf; mapping(address => uint256) public mintedAmounts; @@ -32,8 +28,22 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { MockERC1155Token mockERC1155; MockERC1155Token mockMintToken; + uint256 slicerId = 0; uint256[] productIds = [1, 2, 3, 4]; - uint256 constant USDC_PRICE = 1000000; // 1 USDC (6 decimals) + uint256 constant variantId = 0; + uint256 constant variantIdAlt = 1; + + function _basePrice(uint256 slicerId_, uint256 productId_, uint256 quantity) internal view returns (uint256) { + return _basePriceForVariant(slicerId_, productId_, variantId, quantity); + } + + function _basePriceForVariant(uint256 slicerId_, uint256 productId_, uint256 variantId_, uint256 quantity) + internal + view + returns (uint256) + { + return PRODUCTS_MODULE.basePrice(slicerId_, productId_, variantId_, ETH, quantity); + } function setUp() public { firstForFree = new FirstForFree(PRODUCTS_MODULE); @@ -52,8 +62,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[0], + variantId, abi.encode( - USDC_PRICE, // usdcPrice noConditions, // eligibleTokens (empty) address(mockMintToken), // mintToken uint88(1), // mintTokenId @@ -73,8 +83,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[1], + variantId, abi.encode( - USDC_PRICE * 2, // usdcPrice erc721Conditions, // eligibleTokens address(0), // mintToken (no minting) uint88(0), // mintTokenId @@ -94,8 +104,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[2], + variantId, abi.encode( - USDC_PRICE / 2, // usdcPrice erc1155Conditions, // eligibleTokens address(mockMintToken), // mintToken uint88(2), // mintTokenId @@ -117,8 +127,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[3], + variantId, abi.encode( - USDC_PRICE * 3, // usdcPrice multipleConditions, // eligibleTokens address(mockMintToken), // mintToken uint88(3), // mintTokenId @@ -129,38 +139,59 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { vm.stopPrank(); // Verify product 1 configuration - (uint256 usdcPrice1, address mintToken1, uint88 mintTokenId1, uint8 freeUnits1) = - firstForFree.usdcPrices(slicerId, productIds[0]); - assertEq(usdcPrice1, USDC_PRICE); + (address mintToken1, uint88 mintTokenId1, uint8 freeUnits1) = + firstForFree.productParams(slicerId, productIds[0], variantId); assertEq(mintToken1, address(mockMintToken)); assertEq(mintTokenId1, 1); assertEq(freeUnits1, 3); // Verify product 2 configuration - (uint256 usdcPrice2, address mintToken2, uint88 mintTokenId2, uint8 freeUnits2) = - firstForFree.usdcPrices(slicerId, productIds[1]); - assertEq(usdcPrice2, USDC_PRICE * 2); + (address mintToken2, uint88 mintTokenId2, uint8 freeUnits2) = + firstForFree.productParams(slicerId, productIds[1], variantId); assertEq(mintToken2, address(0)); assertEq(mintTokenId2, 0); assertEq(freeUnits2, 2); // Verify product 3 configuration - (uint256 usdcPrice3, address mintToken3, uint88 mintTokenId3, uint8 freeUnits3) = - firstForFree.usdcPrices(slicerId, productIds[2]); - assertEq(usdcPrice3, USDC_PRICE / 2); + (address mintToken3, uint88 mintTokenId3, uint8 freeUnits3) = + firstForFree.productParams(slicerId, productIds[2], variantId); assertEq(mintToken3, address(mockMintToken)); assertEq(mintTokenId3, 2); assertEq(freeUnits3, 1); // Verify product 4 configuration - (uint256 usdcPrice4, address mintToken4, uint88 mintTokenId4, uint8 freeUnits4) = - firstForFree.usdcPrices(slicerId, productIds[3]); - assertEq(usdcPrice4, USDC_PRICE * 3); + (address mintToken4, uint88 mintTokenId4, uint8 freeUnits4) = + firstForFree.productParams(slicerId, productIds[3], variantId); assertEq(mintToken4, address(mockMintToken)); assertEq(mintTokenId4, 3); assertEq(freeUnits4, 5); } + function testConfigureProduct_variantSpecific() public { + TokenCondition[] memory noConditions = new TokenCondition[](0); + + vm.startPrank(productOwner); + firstForFree.configureProduct( + slicerId, productIds[0], variantId, abi.encode(noConditions, address(0), uint88(0), uint8(1)) + ); + firstForFree.configureProduct( + slicerId, productIds[0], variantIdAlt, abi.encode(noConditions, address(mockMintToken), uint88(7), uint8(2)) + ); + vm.stopPrank(); + + (address mintTokenBase, uint88 mintTokenIdBase, uint8 freeUnitsBase) = + firstForFree.productParams(slicerId, productIds[0], variantId); + assertEq(mintTokenBase, address(0)); + assertEq(mintTokenIdBase, 0); + assertEq(freeUnitsBase, 1); + + (address mintTokenAlt, uint88 mintTokenIdAlt, uint8 freeUnitsAlt) = + firstForFree.productParams(slicerId, productIds[0], variantIdAlt); + assertEq(mintTokenAlt, address(mockMintToken)); + assertEq(mintTokenIdAlt, 7); + assertEq(freeUnitsAlt, 2); + } + function testProductPrice_NoConditions() public { vm.prank(productOwner); @@ -169,8 +200,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[0], + variantId, abi.encode( - USDC_PRICE, // usdcPrice noConditions, // eligibleTokens (empty) address(0), // mintToken uint88(0), // mintTokenId @@ -179,41 +210,86 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { ); // First purchase - should be free - (uint256 ethPrice, uint256 currencyPrice) = - firstForFree.productPrice(slicerId, productIds[0], address(0), 1, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, 0); + uint256 basePrice = _basePrice(slicerId, productIds[0], 1); + uint256 price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, buyer, ""); + assertEq(price, 0); // Second purchase - should be free - (ethPrice, currencyPrice) = firstForFree.productPrice(slicerId, productIds[0], address(0), 1, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, 0); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, buyer, ""); + assertEq(price, 0); // Partial free purchase - (ethPrice, currencyPrice) = firstForFree.productPrice(slicerId, productIds[0], address(0), 2, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, 0); + uint256 twoUnitBasePrice = _basePrice(slicerId, productIds[0], 2); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, twoUnitBasePrice, 2, buyer, ""); + assertEq(price, 0); // Purchase exceeding free units - (ethPrice, currencyPrice) = firstForFree.productPrice(slicerId, productIds[0], address(0), 3, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, USDC_PRICE); // 1 paid unit + uint256 threeUnitBasePrice = _basePrice(slicerId, productIds[0], 3); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, threeUnitBasePrice, 3, buyer, ""); + assertEq(price, _basePrice(slicerId, productIds[0], 1)); // Purchase after using some free units (simulate 1 purchase made) - vm.prank(address(PRODUCTS_MODULE)); - firstForFree.onProductPurchase(slicerId, productIds[0], buyer, 1, "", ""); + vm.mockCall( + address(PRODUCTS_MODULE), + abi.encodeWithSelector(PRODUCTS_MODULE.getPurchases.selector, buyer, slicerId, productIds[0], variantId), + abi.encode(1, "") + ); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, twoUnitBasePrice, 2, buyer, ""); + assertEq(price, _basePrice(slicerId, productIds[0], 1)); - (ethPrice, currencyPrice) = firstForFree.productPrice(slicerId, productIds[0], address(0), 2, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, USDC_PRICE); // 1 free, 1 paid + vm.mockCall( + address(PRODUCTS_MODULE), + abi.encodeWithSelector(PRODUCTS_MODULE.getPurchases.selector, buyer, slicerId, productIds[0], variantId), + abi.encode(2, "") + ); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, buyer, ""); + assertEq(price, basePrice); + } - // Purchase after using all free units (simulate 2 total purchases made) - vm.prank(address(PRODUCTS_MODULE)); - firstForFree.onProductPurchase(slicerId, productIds[0], buyer, 1, "", ""); + function testProductPrice_variantSpecificConfiguration() public { + TokenCondition[] memory noConditions = new TokenCondition[](0); - (ethPrice, currencyPrice) = firstForFree.productPrice(slicerId, productIds[0], address(0), 1, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, USDC_PRICE); // All paid + vm.startPrank(productOwner); + firstForFree.configureProduct( + slicerId, productIds[0], variantId, abi.encode(noConditions, address(0), uint88(0), uint8(1)) + ); + firstForFree.configureProduct( + slicerId, productIds[0], variantIdAlt, abi.encode(noConditions, address(0), uint88(0), uint8(2)) + ); + vm.stopPrank(); + + uint256 basePriceVariant0 = _basePrice(slicerId, productIds[0], 1); + uint256 priceVariant0 = + firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePriceVariant0, 1, buyer, ""); + assertEq(priceVariant0, 0); + + vm.mockCall( + address(PRODUCTS_MODULE), + abi.encodeWithSelector(PRODUCTS_MODULE.getPurchases.selector, buyer, slicerId, productIds[0], variantId), + abi.encode(1, "") + ); + priceVariant0 = + firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePriceVariant0, 1, buyer, ""); + assertEq(priceVariant0, basePriceVariant0); + + uint256 basePriceVariantAlt = _basePriceForVariant(slicerId, productIds[0], variantIdAlt, 1); + uint256 priceVariantAlt = + firstForFree.productPrice(slicerId, productIds[0], variantIdAlt, ETH, basePriceVariantAlt, 1, buyer, ""); + assertEq(priceVariantAlt, 0); + + uint256 twoUnitBasePriceAlt = _basePriceForVariant(slicerId, productIds[0], variantIdAlt, 2); + priceVariantAlt = + firstForFree.productPrice(slicerId, productIds[0], variantIdAlt, ETH, twoUnitBasePriceAlt, 2, buyer, ""); + assertEq(priceVariantAlt, 0); + + vm.mockCall( + address(PRODUCTS_MODULE), + abi.encodeWithSelector(PRODUCTS_MODULE.getPurchases.selector, buyer, slicerId, productIds[0], variantIdAlt), + abi.encode(2, "") + ); + priceVariantAlt = + firstForFree.productPrice(slicerId, productIds[0], variantIdAlt, ETH, basePriceVariantAlt, 1, buyer, ""); + assertEq(priceVariantAlt, basePriceVariantAlt); } function testProductPrice_ERC721Condition() public { @@ -227,8 +303,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[0], + variantId, abi.encode( - USDC_PRICE, // usdcPrice erc721Conditions, // eligibleTokens address(0), // mintToken uint88(0), // mintTokenId @@ -239,28 +315,25 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { vm.stopPrank(); // Buyer without required token - should pay full price - (uint256 ethPrice, uint256 currencyPrice) = - firstForFree.productPrice(slicerId, productIds[0], address(0), 1, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, USDC_PRICE); + uint256 basePrice = _basePrice(slicerId, productIds[0], 1); + uint256 price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, buyer, ""); + assertEq(price, basePrice); // Give buyer the required ERC721 token mockERC721.mint(buyer); // Buyer with required token - should get free units - (ethPrice, currencyPrice) = firstForFree.productPrice(slicerId, productIds[0], address(0), 1, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, 0); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, buyer, ""); + assertEq(price, 0); // Second free purchase - (ethPrice, currencyPrice) = firstForFree.productPrice(slicerId, productIds[0], address(0), 1, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, 0); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, buyer, ""); + assertEq(price, 0); // Purchase exceeding free units - (ethPrice, currencyPrice) = firstForFree.productPrice(slicerId, productIds[0], address(0), 3, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, USDC_PRICE); // 2 free, 1 paid + uint256 threeUnitBasePrice = _basePrice(slicerId, productIds[0], 3); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, threeUnitBasePrice, 3, buyer, ""); + assertEq(price, _basePrice(slicerId, productIds[0], 1)); // 2 free, 1 paid } function testProductPrice_ERC1155Condition() public { @@ -278,8 +351,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[0], + variantId, abi.encode( - USDC_PRICE, // usdcPrice erc1155Conditions, // eligibleTokens address(0), // mintToken uint88(0), // mintTokenId @@ -291,24 +364,22 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { // Buyer without sufficient tokens - should pay full price mockERC1155.setBalance(buyer, 5, 5); // Less than required - (uint256 ethPrice, uint256 currencyPrice) = - firstForFree.productPrice(slicerId, productIds[0], address(0), 1, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, USDC_PRICE); + uint256 basePrice = _basePrice(slicerId, productIds[0], 1); + uint256 price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, buyer, ""); + assertEq(price, basePrice); // Give buyer sufficient tokens mockERC1155.setBalance(buyer, 5, 15); // More than required // Buyer with sufficient tokens - should get free units - (ethPrice, currencyPrice) = firstForFree.productPrice(slicerId, productIds[0], address(0), 2, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, 0); + uint256 twoUnitBasePrice = _basePrice(slicerId, productIds[0], 2); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, twoUnitBasePrice, 2, buyer, ""); + assertEq(price, 0); // Purchase exactly at minimum requirement mockERC1155.setBalance(buyer, 5, 10); // Exactly required amount - (ethPrice, currencyPrice) = firstForFree.productPrice(slicerId, productIds[0], address(0), 1, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, 0); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, buyer, ""); + assertEq(price, 0); } function testProductPrice_MultipleConditions() public { @@ -328,8 +399,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[0], + variantId, abi.encode( - USDC_PRICE, // usdcPrice multipleConditions, // eligibleTokens address(0), // mintToken uint88(0), // mintTokenId @@ -339,31 +410,27 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { vm.stopPrank(); + uint256 basePrice = _basePrice(slicerId, productIds[0], 1); + // Buyer meets first condition only mockERC721.mint(buyer); - (uint256 ethPrice, uint256 currencyPrice) = - firstForFree.productPrice(slicerId, productIds[0], address(0), 1, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, 0); + uint256 price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, buyer, ""); + assertEq(price, 0); // Buyer meets second condition only mockERC1155.setBalance(buyer2, 3, 10); - (ethPrice, currencyPrice) = firstForFree.productPrice(slicerId, productIds[0], address(0), 1, buyer2, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, 0); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, buyer2, ""); + assertEq(price, 0); // Buyer meets both conditions mockERC721.mint(buyer3); mockERC1155.setBalance(buyer3, 3, 10); - (ethPrice, currencyPrice) = firstForFree.productPrice(slicerId, productIds[0], address(0), 1, buyer3, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, 0); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, buyer3, ""); + assertEq(price, 0); // Buyer meets neither condition - (ethPrice, currencyPrice) = - firstForFree.productPrice(slicerId, productIds[0], address(0), 1, address(0x999), ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, USDC_PRICE); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, address(0x999), ""); + assertEq(price, basePrice); } function testOnProductPurchase_WithMinting() public { @@ -373,8 +440,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[0], + variantId, abi.encode( - USDC_PRICE, // usdcPrice noConditions, // eligibleTokens address(mockMintToken), // mintToken uint88(5), // mintTokenId @@ -384,33 +451,26 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { vm.stopPrank(); - // Test purchase tracking and minting - assertEq(firstForFree.purchases(buyer, slicerId), 0); + // Test minting assertEq(mockMintToken.balanceOf(buyer, 5), 0); vm.prank(address(PRODUCTS_MODULE)); - firstForFree.onProductPurchase(slicerId, productIds[0], buyer, 3, "", ""); + firstForFree.onProductPurchase(slicerId, productIds[0], variantId, buyer, 3, ""); - // Check purchase tracking - assertEq(firstForFree.purchases(buyer, slicerId), 3); // Check minting assertEq(mockMintToken.balanceOf(buyer, 5), 3); // Second purchase vm.prank(address(PRODUCTS_MODULE)); - firstForFree.onProductPurchase(slicerId, productIds[0], buyer, 2, "", ""); + firstForFree.onProductPurchase(slicerId, productIds[0], variantId, buyer, 2, ""); - assertEq(firstForFree.purchases(buyer, slicerId), 5); assertEq(mockMintToken.balanceOf(buyer, 5), 5); // Different buyer should have separate tracking vm.prank(address(PRODUCTS_MODULE)); - firstForFree.onProductPurchase(slicerId, productIds[0], buyer2, 1, "", ""); + firstForFree.onProductPurchase(slicerId, productIds[0], variantId, buyer2, 1, ""); - assertEq(firstForFree.purchases(buyer2, slicerId), 1); assertEq(mockMintToken.balanceOf(buyer2, 5), 1); - // Original buyer unchanged - assertEq(firstForFree.purchases(buyer, slicerId), 5); } function testOnProductPurchase_WithoutMinting() public { @@ -420,8 +480,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[0], + variantId, abi.encode( - USDC_PRICE, // usdcPrice noConditions, // eligibleTokens address(0), // mintToken (no minting) uint88(0), // mintTokenId @@ -429,13 +489,10 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { ) ); - assertEq(firstForFree.purchases(buyer, slicerId), 0); - vm.prank(address(PRODUCTS_MODULE)); - firstForFree.onProductPurchase(slicerId, productIds[0], buyer, 2, "", ""); + firstForFree.onProductPurchase(slicerId, productIds[0], variantId, buyer, 2, ""); - // Only purchase tracking, no minting - assertEq(firstForFree.purchases(buyer, slicerId), 2); + // Check no minting assertEq(mockMintToken.balanceOf(buyer, 0), 0); } @@ -448,36 +505,37 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( 0, // slicerId 0 productIds[0], - abi.encode(USDC_PRICE, noConditions, address(0), uint88(0), uint8(2)) + variantId, + abi.encode(noConditions, address(0), uint88(0), uint8(2)) ); firstForFree.configureProduct( 1, // slicerId 1 productIds[0], - abi.encode(USDC_PRICE, noConditions, address(0), uint88(0), uint8(3)) + variantId, + abi.encode(noConditions, address(0), uint88(0), uint8(3)) ); vm.stopPrank(); - // Purchase on slicer 0 - vm.prank(address(PRODUCTS_MODULE)); - firstForFree.onProductPurchase(0, productIds[0], buyer, 2, "", ""); - - // Purchase on slicer 1 - vm.prank(address(PRODUCTS_MODULE)); - firstForFree.onProductPurchase(1, productIds[0], buyer, 1, "", ""); - - // Check separate tracking - assertEq(firstForFree.purchases(buyer, 0), 2); - assertEq(firstForFree.purchases(buyer, 1), 1); - // Verify pricing considers separate tracking - (uint256 ethPrice, uint256 currencyPrice) = - firstForFree.productPrice(0, productIds[0], address(0), 1, buyer, ""); - assertEq(currencyPrice, USDC_PRICE); // No free units left on slicer 0 - - (ethPrice, currencyPrice) = firstForFree.productPrice(1, productIds[0], address(0), 1, buyer, ""); - assertEq(currencyPrice, 0); // Still has free units on slicer 1 + uint256 basePriceSlicer0 = _basePrice(0, productIds[0], 1); + vm.mockCall( + address(PRODUCTS_MODULE), + abi.encodeWithSelector(PRODUCTS_MODULE.getPurchases.selector, buyer, 0, productIds[0], variantId), + abi.encode(2, "") + ); + uint256 price = firstForFree.productPrice(0, productIds[0], variantId, ETH, basePriceSlicer0, 1, buyer, ""); + assertEq(price, basePriceSlicer0); // No free units left on slicer 0 + + uint256 basePriceSlicer1 = _basePrice(1, productIds[0], 1); + vm.mockCall( + address(PRODUCTS_MODULE), + abi.encodeWithSelector(PRODUCTS_MODULE.getPurchases.selector, buyer, 1, productIds[0], variantId), + abi.encode(1, "") + ); + price = firstForFree.productPrice(1, productIds[0], variantId, ETH, basePriceSlicer1, 1, buyer, ""); + assertEq(price, 0); // Still has free units on slicer 1 } function testEdgeCases() public { @@ -487,8 +545,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[0], + variantId, abi.encode( - USDC_PRICE, // usdcPrice noConditions, // eligibleTokens address(0), // mintToken uint88(0), // mintTokenId @@ -497,15 +555,13 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { ); // Zero free units - should always pay - (uint256 ethPrice, uint256 currencyPrice) = - firstForFree.productPrice(slicerId, productIds[0], address(0), 1, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, USDC_PRICE); + uint256 basePrice = _basePrice(slicerId, productIds[0], 1); + uint256 price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, buyer, ""); + assertEq(price, basePrice); // Zero quantity - should return zero - (ethPrice, currencyPrice) = firstForFree.productPrice(slicerId, productIds[0], address(0), 0, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, 0); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, 0, 0, buyer, ""); + assertEq(price, 0); } function testConfigureProduct_UpdateExisting() public { @@ -519,8 +575,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[0], + variantId, abi.encode( - USDC_PRICE, // usdcPrice initialConditions, // eligibleTokens address(mockMintToken), // mintToken uint88(1), // mintTokenId @@ -542,8 +598,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[0], + variantId, abi.encode( - USDC_PRICE * 2, // usdcPrice (updated) newConditions, // eligibleTokens (updated) address(0), // mintToken (updated to none) uint88(0), // mintTokenId @@ -554,9 +610,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { vm.stopPrank(); // Verify updated configuration - (uint256 updatedPrice, address updatedMintToken, uint88 updatedMintTokenId, uint8 updatedFreeUnits) = - firstForFree.usdcPrices(slicerId, productIds[0]); - assertEq(updatedPrice, USDC_PRICE * 2); + (address updatedMintToken, uint88 updatedMintTokenId, uint8 updatedFreeUnits) = + firstForFree.productParams(slicerId, productIds[0], variantId); assertEq(updatedMintToken, address(0)); assertEq(updatedMintTokenId, 0); assertEq(updatedFreeUnits, 5); diff --git a/test/utils/ProductActionTest.sol b/test/utils/ProductActionTest.sol index ef4f28e..5bb5bc5 100644 --- a/test/utils/ProductActionTest.sol +++ b/test/utils/ProductActionTest.sol @@ -11,7 +11,7 @@ abstract contract ProductActionTest is HookTest { function testRevert_onProductPurchase_NotPurchase() public { vm.expectRevert(abi.encodeWithSelector(ProductAction.NotPurchase.selector)); - IProductAction(hook).onProductPurchase(0, 0, address(0), 0, "", ""); + IProductAction(hook).onProductPurchase(0, 0, 0, address(0), 0, ""); } function testRevert_onProductPurchase_WrongSlicer() public { @@ -19,6 +19,6 @@ abstract contract ProductActionTest is HookTest { vm.expectRevert(abi.encodeWithSelector(ProductAction.WrongSlicer.selector)); vm.prank(address(PRODUCTS_MODULE)); - IProductAction(hook).onProductPurchase(unauthorizedSlicer, 0, address(0), 0, "", ""); + IProductAction(hook).onProductPurchase(unauthorizedSlicer, 0, 0, address(0), 0, ""); } } diff --git a/test/utils/HookRegistryTest.sol b/test/utils/ProductHookRegistryTest.sol similarity index 55% rename from test/utils/HookRegistryTest.sol rename to test/utils/ProductHookRegistryTest.sol index f381db1..b30a5c1 100644 --- a/test/utils/HookRegistryTest.sol +++ b/test/utils/ProductHookRegistryTest.sol @@ -2,22 +2,30 @@ pragma solidity ^0.8.30; import {HookTest} from "./HookTest.sol"; -import {IHookRegistry} from "@/utils/RegistryProductPrice.sol"; +import {IProductHookRegistry} from "@/utils/RegistryProductPrice.sol"; import {MockProductsModule} from "./mocks/MockProductsModule.sol"; import {SliceContext} from "@/utils/RegistryProductAction.sol"; -abstract contract HookRegistryTest is HookTest { - function testParamsSchema() public view { - string memory schema = IHookRegistry(hook).paramsSchema(); +bytes32 constant PRODUCT_MANAGER_ROLE = bytes32(uint256(0x04)); + +abstract contract ProductHookRegistryTest is HookTest { + function testConfigureProductSchema() public view { + string memory schema = IProductHookRegistry(hook).configureProductSchema(); assertTrue(bytes(schema).length > 0); } function testConfigureProduct_AccessControl() public { - vm.expectRevert(abi.encodeWithSelector(SliceContext.NotProductOwner.selector)); - IHookRegistry(hook).configureProduct(0, 0, ""); + vm.expectRevert( + abi.encodeWithSelector( + SliceContext.NotAuthorized.selector, + bytes32(uint256(1 << 1 | 1 << 3)) /* Role.ADMIN.getMask() | Role.PRODUCT_MANAGER.getMask() */ + ) + ); + + IProductHookRegistry(hook).configureProduct(0, 0, 0, ""); } - // TODO: verify paramsSchema effectively corresponds to the params + // TODO: verify configureProductSchema effectively corresponds to the params // Blocker: generate bytes params based on a generic string schema // function generateParamsFromSchema(string memory schema) public returns (bytes memory) { diff --git a/test/utils/ProductPriceActionTest.sol b/test/utils/ProductPriceActionTest.sol index cbf0476..9990e2a 100644 --- a/test/utils/ProductPriceActionTest.sol +++ b/test/utils/ProductPriceActionTest.sol @@ -12,7 +12,7 @@ abstract contract ProductPriceActionTest is HookTest { function testRevert_onProductPurchase_NotPurchase() public { vm.expectRevert(abi.encodeWithSelector(ProductAction.NotPurchase.selector)); - IProductAction(hook).onProductPurchase(0, 0, address(0), 0, "", ""); + IProductAction(hook).onProductPurchase(0, 0, 0, address(0), 0, ""); } function testRevert_onProductPurchase_WrongSlicer() public { @@ -20,6 +20,6 @@ abstract contract ProductPriceActionTest is HookTest { vm.expectRevert(abi.encodeWithSelector(ProductAction.WrongSlicer.selector)); vm.prank(address(PRODUCTS_MODULE)); - IProductAction(hook).onProductPurchase(unauthorizedSlicer, 0, address(0), 0, "", ""); + IProductAction(hook).onProductPurchase(unauthorizedSlicer, 0, 0, address(0), 0, ""); } } diff --git a/test/utils/RegistryProductActionTest.sol b/test/utils/RegistryProductActionTest.sol index 0205cbe..2adbbf3 100644 --- a/test/utils/RegistryProductActionTest.sol +++ b/test/utils/RegistryProductActionTest.sol @@ -1,17 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import {HookRegistryTest} from "./HookRegistryTest.sol"; -import {RegistryProductAction, IHookRegistry, IProductAction} from "@/utils/RegistryProductAction.sol"; +import {ProductHookRegistryTest} from "./ProductHookRegistryTest.sol"; +import {RegistryProductAction, IProductHookRegistry, IProductAction} from "@/utils/RegistryProductAction.sol"; -abstract contract RegistryProductActionTest is HookRegistryTest { +abstract contract RegistryProductActionTest is ProductHookRegistryTest { function testSupportsInterface_RegistryProductAction() public view { assertTrue(IProductAction(hook).supportsInterface(type(IProductAction).interfaceId)); - assertTrue(IProductAction(hook).supportsInterface(type(IHookRegistry).interfaceId)); + assertTrue(IProductAction(hook).supportsInterface(type(IProductHookRegistry).interfaceId)); } function testRevert_onProductPurchase_NotPurchase() public { vm.expectRevert(abi.encodeWithSelector(RegistryProductAction.NotPurchase.selector)); - IProductAction(hook).onProductPurchase(0, 0, address(0), 0, "", ""); + IProductAction(hook).onProductPurchase(0, 0, 0, address(0), 0, ""); } } diff --git a/test/utils/RegistryProductPriceActionTest.sol b/test/utils/RegistryProductPriceActionTest.sol index 56b48dd..7b3cbd6 100644 --- a/test/utils/RegistryProductPriceActionTest.sol +++ b/test/utils/RegistryProductPriceActionTest.sol @@ -1,24 +1,24 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import {HookRegistryTest} from "./HookRegistryTest.sol"; +import {ProductHookRegistryTest} from "./ProductHookRegistryTest.sol"; import { RegistryProductPriceAction, RegistryProductAction, - IHookRegistry, + IProductHookRegistry, IProductAction, IProductPrice } from "@/utils/RegistryProductPriceAction.sol"; -abstract contract RegistryProductPriceActionTest is HookRegistryTest { +abstract contract RegistryProductPriceActionTest is ProductHookRegistryTest { function testSupportsInterface_RegistryProductPriceAction() public view { assertTrue(IProductAction(hook).supportsInterface(type(IProductAction).interfaceId)); assertTrue(IProductAction(hook).supportsInterface(type(IProductPrice).interfaceId)); - assertTrue(IProductAction(hook).supportsInterface(type(IHookRegistry).interfaceId)); + assertTrue(IProductAction(hook).supportsInterface(type(IProductHookRegistry).interfaceId)); } function testRevert_onProductPurchase_NotPurchase() public { vm.expectRevert(abi.encodeWithSelector(RegistryProductAction.NotPurchase.selector)); - IProductAction(hook).onProductPurchase(0, 0, address(0), 0, "", ""); + IProductAction(hook).onProductPurchase(0, 0, 0, address(0), 0, ""); } } diff --git a/test/utils/RegistryProductPriceTest.sol b/test/utils/RegistryProductPriceTest.sol index 6d236e6..1628687 100644 --- a/test/utils/RegistryProductPriceTest.sol +++ b/test/utils/RegistryProductPriceTest.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import {HookRegistryTest} from "./HookRegistryTest.sol"; -import {RegistryProductPrice, IHookRegistry, IProductPrice} from "@/utils/RegistryProductPrice.sol"; +import {ProductHookRegistryTest} from "./ProductHookRegistryTest.sol"; +import {RegistryProductPrice, IProductHookRegistry, IProductPrice} from "@/utils/RegistryProductPrice.sol"; -abstract contract RegistryProductPriceTest is HookRegistryTest { +abstract contract RegistryProductPriceTest is ProductHookRegistryTest { function testSupportsInterface_RegistryProductPrice() public view { assertTrue(IProductPrice(hook).supportsInterface(type(IProductPrice).interfaceId)); - assertTrue(IProductPrice(hook).supportsInterface(type(IHookRegistry).interfaceId)); + assertTrue(IProductPrice(hook).supportsInterface(type(IProductHookRegistry).interfaceId)); } } diff --git a/test/utils/mocks/MockProductsModule.sol b/test/utils/mocks/MockProductsModule.sol index 0b8a4a9..2ea1328 100644 --- a/test/utils/mocks/MockProductsModule.sol +++ b/test/utils/mocks/MockProductsModule.sol @@ -2,26 +2,50 @@ pragma solidity ^0.8.30; // import {IProductsModule} from "@/utils/ProductAction.sol"; +import {Price} from "slice/types.sol"; import {Test} from "forge-std/Test.sol"; contract MockProductsModule is Test // , IProductsModule { + error NotAuthorized(bytes32 rolesMask); + + function checkRoles(uint256, bytes32, address account) public pure { + if (account != vm.addr(uint256(keccak256(abi.encodePacked("productOwner"))))) { + revert NotAuthorized(bytes32(uint256(1 << 1 | 1 << 3))); + } + } + function isProductOwner(uint256, uint256, address account) external pure returns (bool isAllowed) { isAllowed = account == vm.addr(uint256(keccak256(abi.encodePacked("productOwner")))); } - function basePrice(uint256, uint256, address, uint256) + function basePrice(uint256, uint256, uint256, address currency, uint256 quantity) external pure - returns (uint256 ethPrice, uint256 currencyPrice) + returns (uint256 amount) { - ethPrice = 1e16; - currencyPrice = 100e18; + if (currency == address(0)) { + amount = 1e16 * quantity; + } else { + // USDC + amount = 100e6 * quantity; + } } - function availableUnits(uint256, uint256) external pure returns (uint256 units, bool isInfinite) { + function stockUnits(uint256, uint256, uint256) external pure returns (uint256 units, bool isInfinite) { units = 6392; isInfinite = false; } + + function getPurchases(address, uint256, uint256, uint256) + external + pure + returns (uint256 purchasedUnits, bytes memory purchaseData) + { + purchasedUnits = 0; + purchaseData = ""; + } + + function productExists(uint256 slicerId, uint256 productId) external view {} }