From f34cd17eb3ee32057aee57cd7445b5f9064795d0 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Fri, 29 Aug 2025 17:14:44 +0200 Subject: [PATCH 01/30] chore: upgrade checkout to v5 --- .github/workflows/subtree-sync.yml | 2 +- .github/workflows/tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 From 9b8a80a60711ff55848ba3befa77deb72c8430c2 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Tue, 9 Sep 2025 17:54:54 +0200 Subject: [PATCH 02/30] fix: contracts --- script/Seed.s.sol | 22 +-- src/custom/actions/BaseCafe/BaseCafe.sol | 2 +- .../actions/BaseGirlsScout/BaseGirlsScout.sol | 2 +- src/hooks/actions/Allowlisted/Allowlisted.sol | 2 +- src/hooks/actions/ERC20Gated/ERC20Gated.sol | 2 +- src/hooks/actions/ERC20Mint/ERC20Mint.sol | 17 +- src/hooks/actions/ERC721Mint/ERC721Mint.sol | 2 +- src/hooks/actions/NFTGated/NFTGated.sol | 2 +- .../NFTDiscount/NFTDiscount.sol | 11 +- .../pricing/TieredDiscount/TieredDiscount.sol | 23 ++- .../LinearVRGDAPrices/LinearVRGDAPrices.sol | 12 +- .../LogisticVRGDAPrices.sol | 12 +- .../FirstForFree/FirstForFree.sol | 24 +-- test/actions/Allowlisted/Allowlisted.t.sol | 4 +- test/actions/ERC20Gated/ERC20Gated.t.sol | 4 +- test/actions/ERC20Mint/ERC20Mint.t.sol | 26 ++-- test/actions/ERC721Mint/ERC721Mint.t.sol | 22 +-- test/actions/NFTGated/NFTGated.t.sol | 24 +-- test/pricing/TieredDiscount/NFTDiscount.t.sol | 115 +++++++------- test/pricing/VRGDA/LinearVRGDA.t.sol | 32 ++-- test/pricing/VRGDA/LogisticVRGDA.t.sol | 32 ++-- .../FirstForFree/FirstForFree.t.sol | 147 +++++++++--------- test/utils/ProductActionTest.sol | 4 +- test/utils/ProductPriceActionTest.sol | 4 +- test/utils/RegistryProductActionTest.sol | 2 +- test/utils/RegistryProductPriceActionTest.sol | 2 +- test/utils/mocks/MockProductsModule.sol | 13 +- 27 files changed, 277 insertions(+), 287 deletions(-) diff --git a/script/Seed.s.sol b/script/Seed.s.sol index 7cc035a..8754d81 100644 --- a/script/Seed.s.sol +++ b/script/Seed.s.sol @@ -2,14 +2,14 @@ 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; @@ -26,39 +26,39 @@ contract SeedHooksScript is Script, CommonStorage { Hook[] memory hooks = new Hook[](9); hooks[0] = Hook({ hookAddress: 0x157428DD791E03c20880D22C3dA2B66A36B5cF26, - code: address(new Allowlisted(PRODUCTS_MODULE())).code + code: address(new Allowlisted(Contracts.PRODUCTS_MODULE)).code }); hooks[1] = Hook({ hookAddress: 0x26A1C86B555013995Fc72864D261fDe984752E7c, - code: address(new ERC20Gated(PRODUCTS_MODULE())).code + code: address(new ERC20Gated(Contracts.PRODUCTS_MODULE)).code }); hooks[2] = Hook({ hookAddress: 0x67f9799FaC1D53C63217BEE47f553150F5BB0836, - code: address(new ERC20Mint(PRODUCTS_MODULE())).code + code: address(new ERC20Mint(Contracts.PRODUCTS_MODULE)).code }); hooks[3] = Hook({ hookAddress: 0x2b6488115FAa50142E140172CbCd60e6370675F7, - code: address(new ERC721Mint(PRODUCTS_MODULE())).code + code: address(new ERC721Mint(Contracts.PRODUCTS_MODULE)).code }); hooks[4] = Hook({ hookAddress: 0xD4eF7A46bF4c58036eaCA886119F5230e5a2C25d, - code: address(new NFTGated(PRODUCTS_MODULE())).code + code: address(new NFTGated(Contracts.PRODUCTS_MODULE)).code }); hooks[5] = Hook({ hookAddress: 0xb830a457d2f51d4cA1136b97FB30DF6366CFe2f5, - code: address(new NFTDiscount(PRODUCTS_MODULE())).code + code: address(new NFTDiscount(Contracts.PRODUCTS_MODULE)).code }); hooks[6] = Hook({ hookAddress: 0xEC68E30182F4298b7032400B7ce809da613e4449, - code: address(new LinearVRGDAPrices(PRODUCTS_MODULE())).code + code: address(new LinearVRGDAPrices(Contracts.PRODUCTS_MODULE)).code }); hooks[7] = Hook({ hookAddress: 0x2b02cC8528EF18abf8185543CEC29A94F0542c8F, - code: address(new LogisticVRGDAPrices(PRODUCTS_MODULE())).code + code: address(new LogisticVRGDAPrices(Contracts.PRODUCTS_MODULE)).code }); hooks[8] = Hook({ hookAddress: 0xEC68E30182F4298b7032400B7ce809da613e4449, - code: address(new FirstForFree(PRODUCTS_MODULE())).code + code: address(new FirstForFree(Contracts.PRODUCTS_MODULE)).code }); // Deploy hooks 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 index 7588422..9e59d48 100644 --- a/src/custom/actions/BaseGirlsScout/BaseGirlsScout.sol +++ b/src/custom/actions/BaseGirlsScout/BaseGirlsScout.sol @@ -44,7 +44,7 @@ contract BaseGirlsScout is ProductAction, Ownable { * @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/hooks/actions/Allowlisted/Allowlisted.sol b/src/hooks/actions/Allowlisted/Allowlisted.sol index ab4e539..83e3187 100644 --- a/src/hooks/actions/Allowlisted/Allowlisted.sol +++ b/src/hooks/actions/Allowlisted/Allowlisted.sol @@ -39,9 +39,9 @@ contract Allowlisted is RegistryProductAction { function isPurchaseAllowed( uint256 slicerId, uint256 productId, + uint256, address buyer, uint256, - bytes memory, bytes memory buyerCustomData ) public view override returns (bool isAllowed) { // Get Merkle proof from buyerCustomData diff --git a/src/hooks/actions/ERC20Gated/ERC20Gated.sol b/src/hooks/actions/ERC20Gated/ERC20Gated.sol index f4964dc..30b8365 100644 --- a/src/hooks/actions/ERC20Gated/ERC20Gated.sol +++ b/src/hooks/actions/ERC20Gated/ERC20Gated.sol @@ -36,7 +36,7 @@ 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, address buyer, uint256, bytes memory) public view override diff --git a/src/hooks/actions/ERC20Mint/ERC20Mint.sol b/src/hooks/actions/ERC20Mint/ERC20Mint.sol index a6ab8d3..8f7a1bc 100644 --- a/src/hooks/actions/ERC20Mint/ERC20Mint.sol +++ b/src/hooks/actions/ERC20Mint/ERC20Mint.sol @@ -44,14 +44,13 @@ contract ERC20Mint is RegistryProductAction { * @inheritdoc IProductAction * @dev If `revertOnMaxSupplyReached` is set to true, returns false when max supply is exceeded. */ - function isPurchaseAllowed( - uint256 slicerId, - uint256 productId, - address, - uint256 quantity, - bytes memory, - bytes memory - ) public view virtual override returns (bool isAllowed) { + function isPurchaseAllowed(uint256 slicerId, uint256 productId, uint256, address, uint256 quantity, bytes memory) + public + view + virtual + override + returns (bool isAllowed) + { ERC20Data memory tokenData_ = tokenData[slicerId][productId]; if (tokenData_.revertOnMaxSupplyReached) { @@ -70,9 +69,9 @@ contract ERC20Mint is RegistryProductAction { function _onProductPurchase( uint256 slicerId, uint256 productId, + uint256, address buyer, uint256 quantity, - bytes memory, bytes memory ) internal override { ERC20Data memory tokenData_ = tokenData[slicerId][productId]; diff --git a/src/hooks/actions/ERC721Mint/ERC721Mint.sol b/src/hooks/actions/ERC721Mint/ERC721Mint.sol index 8d3a18f..82a7651 100644 --- a/src/hooks/actions/ERC721Mint/ERC721Mint.sol +++ b/src/hooks/actions/ERC721Mint/ERC721Mint.sol @@ -43,9 +43,9 @@ contract ERC721Mint is RegistryProductAction { function _onProductPurchase( uint256 slicerId, uint256 productId, + uint256, address buyer, uint256 quantity, - bytes memory, bytes memory ) internal override { ERC721Data memory tokenData_ = tokenData[slicerId][productId]; diff --git a/src/hooks/actions/NFTGated/NFTGated.sol b/src/hooks/actions/NFTGated/NFTGated.sol index b792296..5918ea3 100644 --- a/src/hooks/actions/NFTGated/NFTGated.sol +++ b/src/hooks/actions/NFTGated/NFTGated.sol @@ -38,7 +38,7 @@ 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, address buyer, uint256, bytes memory) public view override diff --git a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol index 26a586f..560cd2e 100644 --- a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol @@ -4,7 +4,7 @@ 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 {DiscountParams, TieredDiscount} from "../TieredDiscount.sol"; +import {DiscountParams, Price, TieredDiscount} from "../TieredDiscount.sol"; import {NFTType} from "../types/DiscountParams.sol"; /** @@ -77,6 +77,7 @@ contract NFTDiscount is TieredDiscount { * @notice Base price is returned if user does not have a discount. */ function _productPrice( + uint256, uint256, uint256, address currency, @@ -85,15 +86,15 @@ contract NFTDiscount is TieredDiscount { bytes memory, uint256 basePrice, DiscountParams[] memory discountParams - ) internal view virtual override returns (uint256 ethPrice, uint256 currencyPrice) { + ) internal view virtual override returns (Price memory price) { uint256 discount = _getHighestDiscount(discountParams, buyer); - uint256 price = discount != 0 ? _getDiscountedPrice(basePrice, discount, quantity) : quantity * basePrice; + uint256 amount = discount != 0 ? _getDiscountedPrice(basePrice, discount, quantity) : quantity * basePrice; if (currency == address(0)) { - ethPrice = price; + price.eth = amount; } else { - currencyPrice = price; + price.currency = amount; } } diff --git a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol index 0d85557..761f38c 100644 --- a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.30; import {IProductsModule} from "slice/interfaces/IProductsModule.sol"; import {RegistryProductPrice, IProductPrice} from "@/utils/RegistryProductPrice.sol"; import {DiscountParams} from "./types/DiscountParams.sol"; +import {Price} from "slice/types/Price.sol"; /** * @title TieredDiscount @@ -42,18 +43,27 @@ abstract contract TieredDiscount is RegistryProductPrice { function productPrice( uint256 slicerId, uint256 productId, + uint256 variantId, address currency, uint256 quantity, 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; + ) public view override returns (Price memory) { + Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, currency, quantity); DiscountParams[] memory discountParams = discounts[slicerId][productId]; - return _productPrice(slicerId, productId, currency, quantity, buyer, data, basePrice, discountParams); + return _productPrice( + slicerId, + productId, + variantId, + currency, + quantity, + buyer, + data, + currency == address(0) ? basePrice.eth : basePrice.currency, + discountParams + ); } /*////////////////////////////////////////////////////////////// @@ -77,11 +87,12 @@ abstract contract TieredDiscount is RegistryProductPrice { function _productPrice( uint256 slicerId, uint256 productId, + uint256 variantId, address currency, uint256 quantity, address buyer, bytes memory data, uint256 basePrice, DiscountParams[] memory discountParams - ) internal view virtual returns (uint256 ethPrice, uint256 currencyPrice); + ) internal view virtual returns (Price memory); } diff --git a/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol index ab0ad08..84150af 100644 --- a/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol @@ -6,6 +6,7 @@ import {wadLn, unsafeWadDiv, toDaysWadUnsafe} from "@/utils/math/SignedWadMath.s import {LinearProductParams} from "../types/LinearProductParams.sol"; import {LinearVRGDAParams} from "../types/LinearVRGDAParams.sol"; import {VRGDAPrices} from "../VRGDAPrices.sol"; +import {Price} from "slice/types/Price.sol"; /// @title LinearVRGDAPrices /// @notice VRGDA with a linear issuance curve - Price library with different params for each Slice product. @@ -43,7 +44,7 @@ contract LinearVRGDAPrices is VRGDAPrices { /// Get product availability and isInfinite /// @dev available units is a uint32 - (uint256 availableUnits, bool isInfinite) = PRODUCTS_MODULE.availableUnits(slicerId, productId); + (uint256 availableUnits, bool isInfinite) = PRODUCTS_MODULE.stockUnits(slicerId, productId, 0); // Product must not have infinite availability require(!isInfinite, "NOT_FINITE_AVAILABILITY"); @@ -69,11 +70,12 @@ contract LinearVRGDAPrices is VRGDAPrices { function productPrice( uint256 slicerId, uint256 productId, + uint256 variantId, address currency, uint256 quantity, address, bytes memory - ) public view override returns (uint256 ethPrice, uint256 currencyPrice) { + ) public view override returns (Price memory price) { // Add reference for product and pricing params LinearProductParams storage productParams = _productParams[slicerId][productId]; LinearVRGDAParams memory pricingParams = productParams.pricingParams[currency]; @@ -81,14 +83,14 @@ contract LinearVRGDAPrices is VRGDAPrices { require(productParams.startTime != 0, "PRODUCT_UNSET"); // Get available units - (uint256 availableUnits,) = PRODUCTS_MODULE.availableUnits(slicerId, productId); + (uint256 availableUnits,) = PRODUCTS_MODULE.stockUnits(slicerId, productId, variantId); // Calculate sold units from availableUnits uint256 soldUnits = productParams.startUnits - availableUnits; // Set ethPrice or currencyPrice based on chosen currency if (currency == address(0)) { - ethPrice = getAdjustedVRGDAPrice( + price.eth = getAdjustedVRGDAPrice( pricingParams.targetPrice, productParams.decayConstant, toDaysWadUnsafe(block.timestamp - productParams.startTime), @@ -98,7 +100,7 @@ contract LinearVRGDAPrices is VRGDAPrices { quantity ); } else { - currencyPrice = getAdjustedVRGDAPrice( + price.currency = getAdjustedVRGDAPrice( pricingParams.targetPrice, productParams.decayConstant, toDaysWadUnsafe(block.timestamp - productParams.startTime), diff --git a/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol b/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol index 5a0a114..c2175ea 100644 --- a/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol @@ -15,6 +15,7 @@ import { import {LogisticProductParams} from "../types/LogisticProductParams.sol"; import {LogisticVRGDAParams} from "../types/LogisticVRGDAParams.sol"; import {IProductsModule, VRGDAPrices} from "../VRGDAPrices.sol"; +import {Price} from "slice/types/Price.sol"; /// @title LogisticVRGDAPrices /// @notice VRGDA with a logistic issuance curve - Price library with different params for each Slice product. @@ -51,7 +52,7 @@ 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); + (uint256 availableUnits, bool isInfinite) = PRODUCTS_MODULE.stockUnits(slicerId, productId, 0); // Product must not have infinite availability require(!isInfinite, "NON_FINITE_AVAILABILITY"); @@ -77,11 +78,12 @@ contract LogisticVRGDAPrices is VRGDAPrices { function productPrice( uint256 slicerId, uint256 productId, + uint256 variantId, address currency, uint256 quantity, address, bytes memory - ) public view override returns (uint256 ethPrice, uint256 currencyPrice) { + ) public view override returns (Price memory price) { // Add reference for product and pricing params LogisticProductParams storage productParams = _productParams[slicerId][productId]; LogisticVRGDAParams memory pricingParams = productParams.pricingParams[currency]; @@ -89,11 +91,11 @@ contract LogisticVRGDAPrices is VRGDAPrices { require(productParams.startTime != 0, "PRODUCT_UNSET"); // Get available units - (uint256 availableUnits,) = PRODUCTS_MODULE.availableUnits(slicerId, productId); + (uint256 availableUnits,) = PRODUCTS_MODULE.stockUnits(slicerId, productId, variantId); // Set ethPrice or currencyPrice based on chosen currency if (currency == address(0)) { - ethPrice = getAdjustedVRGDALogisticPrice( + price.eth = getAdjustedVRGDALogisticPrice( pricingParams.targetPrice, productParams.decayConstant, toDaysWadUnsafe(block.timestamp - productParams.startTime), @@ -104,7 +106,7 @@ contract LogisticVRGDAPrices is VRGDAPrices { quantity ); } else { - currencyPrice = getAdjustedVRGDALogisticPrice( + price.currency = getAdjustedVRGDALogisticPrice( pricingParams.targetPrice, productParams.decayConstant, toDaysWadUnsafe(block.timestamp - productParams.startTime), diff --git a/src/hooks/pricingActions/FirstForFree/FirstForFree.sol b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol index ff2f870..3b4046d 100644 --- a/src/hooks/pricingActions/FirstForFree/FirstForFree.sol +++ b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol @@ -14,6 +14,7 @@ import {HookRegistry} from "@/utils/RegistryProductAction.sol"; import {ProductParams, TokenCondition} from "./types/ProductParams.sol"; import {TokenType} from "./types/TokenCondition.sol"; import {ITokenERC1155} from "./utils/ITokenERC1155.sol"; +import {Price} from "slice/types/Price.sol"; /** * @title FirstForFree @@ -42,12 +43,15 @@ 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) - { + function productPrice( + uint256 slicerId, + uint256 productId, + uint256, + address, + uint256 quantity, + address buyer, + bytes memory + ) public view override returns (Price memory price) { ProductParams memory productParams = usdcPrices[slicerId][productId]; if (_isEligible(buyer, productParams.eligibleTokens)) { @@ -56,15 +60,15 @@ contract FirstForFree is RegistryProductPriceAction { unchecked { uint256 freeUnitsLeft = productParams.freeUnits - totalPurchases; if (quantity <= freeUnitsLeft) { - return (0, 0); + return (Price(0, 0)); } else { - return (0, usdcPrices[slicerId][productId].usdcPrice * (quantity - freeUnitsLeft)); + return (Price(0, usdcPrices[slicerId][productId].usdcPrice * (quantity - freeUnitsLeft))); } } } } - return (0, usdcPrices[slicerId][productId].usdcPrice * quantity); + return (Price(0, usdcPrices[slicerId][productId].usdcPrice * quantity)); } /** @@ -74,9 +78,9 @@ contract FirstForFree is RegistryProductPriceAction { function _onProductPurchase( uint256 slicerId, uint256 productId, + uint256, address buyer, uint256 quantity, - bytes memory, bytes memory ) internal override { purchases[buyer][slicerId] += quantity; diff --git a/test/actions/Allowlisted/Allowlisted.t.sol b/test/actions/Allowlisted/Allowlisted.t.sol index a59108f..3c3237d 100644 --- a/test/actions/Allowlisted/Allowlisted.t.sol +++ b/test/actions/Allowlisted/Allowlisted.t.sol @@ -41,7 +41,7 @@ contract AllowlistedTest is RegistryProductActionTest { allowlisted.configureProduct(slicerId, productId, 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, 0, buyer, 0, abi.encode(proof))); } function testIsPurchaseAllowed_wrongProof() public { @@ -51,6 +51,6 @@ contract AllowlistedTest is RegistryProductActionTest { allowlisted.configureProduct(slicerId, productId, 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, 0, buyer, 0, abi.encode(wrongProof))); } } diff --git a/test/actions/ERC20Gated/ERC20Gated.t.sol b/test/actions/ERC20Gated/ERC20Gated.t.sol index a4d8ea3..7fcfed7 100644 --- a/test/actions/ERC20Gated/ERC20Gated.t.sol +++ b/test/actions/ERC20Gated/ERC20Gated.t.sol @@ -64,9 +64,9 @@ contract ERC20GatedTest is RegistryProductActionTest { vm.prank(productOwner); erc20Gated.configureProduct(slicerId, productId, abi.encode(gates)); - assertFalse(erc20Gated.isPurchaseAllowed(slicerId, productId, buyer, 0, "", "")); + assertFalse(erc20Gated.isPurchaseAllowed(slicerId, productId, 0, buyer, 0, "")); token.mint(buyer, 100); - assertTrue(erc20Gated.isPurchaseAllowed(slicerId, productId, buyer, 0, "", "")); + assertTrue(erc20Gated.isPurchaseAllowed(slicerId, productId, 0, buyer, 0, "")); } } diff --git a/test/actions/ERC20Mint/ERC20Mint.t.sol b/test/actions/ERC20Mint/ERC20Mint.t.sol index 48921cc..60d484d 100644 --- a/test/actions/ERC20Mint/ERC20Mint.t.sol +++ b/test/actions/ERC20Mint/ERC20Mint.t.sol @@ -210,15 +210,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 { @@ -263,7 +263,7 @@ contract ERC20MintTest is RegistryProductActionTest { uint256 initialSupply1 = token1.totalSupply(); vm.prank(address(PRODUCTS_MODULE)); - erc20Mint.onProductPurchase(slicerId, productIds[0], buyer, 3, "", ""); + erc20Mint.onProductPurchase(slicerId, productIds[0], 0, buyer, 3, ""); assertEq(token1.balanceOf(buyer), initialBalance1 + 300); // 3 * 100 assertEq(token1.totalSupply(), initialSupply1 + 300); @@ -273,14 +273,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], 0, 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], 0, buyer3, 2, ""); assertEq(token1.balanceOf(buyer3), 200); // 2 * 100 assertEq(token1.totalSupply(), initialSupply1 + 500); // 300 + 200 } @@ -311,7 +311,7 @@ contract ERC20MintTest is RegistryProductActionTest { uint256 initialSupply = token.totalSupply(); vm.prank(address(PRODUCTS_MODULE)); - erc20Mint.onProductPurchase(slicerId, productIds[0], buyer, 2, "", ""); + erc20Mint.onProductPurchase(slicerId, productIds[0], 0, buyer, 2, ""); // Balance and supply should remain unchanged (mint failed silently) assertEq(token.balanceOf(buyer), initialBalance); @@ -340,7 +340,7 @@ 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]); assertEq(token.totalSupply(), 1000); // at max supply @@ -348,6 +348,6 @@ contract ERC20MintTest is RegistryProductActionTest { // 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], 0, buyer, 1, ""); } } diff --git a/test/actions/ERC721Mint/ERC721Mint.t.sol b/test/actions/ERC721Mint/ERC721Mint.t.sol index 9a25bc2..1c7d6f3 100644 --- a/test/actions/ERC721Mint/ERC721Mint.t.sol +++ b/test/actions/ERC721Mint/ERC721Mint.t.sol @@ -224,7 +224,7 @@ contract ERC721MintTest is RegistryProductActionTest { uint256 initialSupply1 = token1.totalSupply(); vm.prank(address(PRODUCTS_MODULE)); - erc721Mint.onProductPurchase(slicerId, productIds[0], buyer, 3, "", ""); + erc721Mint.onProductPurchase(slicerId, productIds[0], 0, buyer, 3, ""); assertEq(token1.balanceOf(buyer), initialBalance1 + 3); assertEq(token1.totalSupply(), initialSupply1 + 3); @@ -234,14 +234,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], 0, 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], 0, buyer3, 2, ""); assertEq(token1.balanceOf(buyer3), 2); assertEq(token1.totalSupply(), initialSupply1 + 5); // 3 + 2 } @@ -269,12 +269,12 @@ 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, ""); 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], 0, buyer2, 2, ""); assertEq(token.totalSupply(), 5); // Third purchase - exceeds max supply but should not revert (mint will fail silently) @@ -282,7 +282,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], 0, buyer3, 2, ""); // Balance and supply should remain unchanged (mint failed silently) assertEq(token.balanceOf(buyer3), balanceBefore); @@ -310,20 +310,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]); 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], 0, 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], 0, buyer3, 1, ""); } function testTokenURI() public { @@ -368,10 +368,10 @@ contract ERC721MintTest is RegistryProductActionTest { // Mint some tokens vm.prank(address(PRODUCTS_MODULE)); - erc721Mint.onProductPurchase(slicerId, productIds[0], buyer, 2, "", ""); + erc721Mint.onProductPurchase(slicerId, productIds[0], 0, buyer, 2, ""); vm.prank(address(PRODUCTS_MODULE)); - erc721Mint.onProductPurchase(slicerId, productIds[1], buyer, 1, "", ""); + erc721Mint.onProductPurchase(slicerId, productIds[1], 0, buyer, 1, ""); // Test token URIs for product 1 (with baseURI) assertEq(token1.tokenURI(0), "https://api.example.com/metadata/0"); diff --git a/test/actions/NFTGated/NFTGated.t.sol b/test/actions/NFTGated/NFTGated.t.sol index 10e2b23..ee633bf 100644 --- a/test/actions/NFTGated/NFTGated.t.sol +++ b/test/actions/NFTGated/NFTGated.t.sol @@ -67,22 +67,22 @@ 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], 0, buyer, 0, "")); + assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[1], 0, buyer, 0, "")); + assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[2], 0, buyer, 0, "")); + assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[3], 0, 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], 0, buyer2, 0, "")); + assertFalse(nftGated.isPurchaseAllowed(slicerId, productIds[1], 0, buyer2, 0, "")); + assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[2], 0, buyer2, 0, "")); + assertFalse(nftGated.isPurchaseAllowed(slicerId, productIds[3], 0, 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], 0, buyer3, 0, "")); + assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[1], 0, buyer3, 0, "")); + assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[2], 0, buyer3, 0, "")); + assertFalse(nftGated.isPurchaseAllowed(slicerId, productIds[3], 0, buyer3, 0, "")); } /*////////////////////////////////////////////////////////////// diff --git a/test/pricing/TieredDiscount/NFTDiscount.t.sol b/test/pricing/TieredDiscount/NFTDiscount.t.sol index 0aaab39..ef2976b 100644 --- a/test/pricing/TieredDiscount/NFTDiscount.t.sol +++ b/test/pricing/TieredDiscount/NFTDiscount.t.sol @@ -6,6 +6,7 @@ import {console2} from "forge-std/console2.sol"; import { IProductsModule, NFTDiscount, + Price, DiscountParams, NFTType } from "@/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol"; @@ -51,13 +52,12 @@ contract NFTDiscountTest is RegistryProductPriceTest { erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + Price memory price = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); - (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, 0, ETH, quantity); - assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); - assertEq(currencyPrice, 0); + assertEq(price.eth, quantity * (basePrice.eth - (basePrice.eth * percentDiscountOne) / 1e4)); + assertEq(price.currency, 0); } function testConfigureProduct__ERC20() public { @@ -76,13 +76,12 @@ contract NFTDiscountTest is RegistryProductPriceTest { erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, USDC, quantity, buyer, ""); + Price memory price = erc721GatedDiscount.productPrice(slicerId, productId, 0, USDC, quantity, buyer, ""); - (, uint256 baseCurrencyPrice) = PRODUCTS_MODULE.basePrice(slicerId, productId, USDC, quantity); + Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, 0, USDC, quantity); - assertEq(currencyPrice, quantity * (baseCurrencyPrice - (baseCurrencyPrice * percentDiscountOne) / 1e4)); - assertTrue(ethPrice == 0); + assertEq(price.currency, quantity * (basePrice.currency - (basePrice.currency * percentDiscountOne) / 1e4)); + assertTrue(price.eth == 0); } function testConfigureProduct__ERC1155() public { @@ -101,20 +100,19 @@ contract NFTDiscountTest is RegistryProductPriceTest { erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, USDC, quantity, buyer, ""); + Price memory price = erc721GatedDiscount.productPrice(slicerId, productId, 0, USDC, quantity, buyer, ""); - (, uint256 baseCurrencyPrice) = PRODUCTS_MODULE.basePrice(slicerId, productId, USDC, quantity); + Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, 0, USDC, quantity); - assertEq(currencyPrice, quantity * baseCurrencyPrice); - assertEq(ethPrice, 0); + assertEq(price.currency, quantity * basePrice.currency); + assertEq(price.eth, 0); nft1155.mint(buyer); - (ethPrice, currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, USDC, quantity, buyer, ""); + price = erc721GatedDiscount.productPrice(slicerId, productId, 0, USDC, quantity, buyer, ""); - assertEq(currencyPrice, quantity * (baseCurrencyPrice - (baseCurrencyPrice * percentDiscountOne) / 1e4)); - assertEq(ethPrice, 0); + assertEq(price.currency, quantity * (basePrice.currency - (basePrice.currency * percentDiscountOne) / 1e4)); + assertEq(price.eth, 0); } function testConfigureProduct__HigherDiscount() public { @@ -139,20 +137,19 @@ contract NFTDiscountTest is RegistryProductPriceTest { erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); /// check product price for ETH - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + Price memory price = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); - (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, 0, ETH, quantity); - assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); - assertEq(currencyPrice, 0); + assertEq(price.eth, quantity * (basePrice.eth - (basePrice.eth * percentDiscountOne) / 1e4)); + assertEq(price.currency, 0); nft1155.mint(buyer); - (ethPrice, currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + price = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); - assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountTwo) / 1e4)); - assertEq(currencyPrice, 0); + assertEq(price.eth, quantity * (basePrice.eth - (basePrice.eth * percentDiscountTwo) / 1e4)); + assertEq(price.currency, 0); } function testRevert_ProductPrice__NotNFTOwner() public { @@ -171,13 +168,12 @@ contract NFTDiscountTest is RegistryProductPriceTest { erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + Price memory price = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); - (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, 0, ETH, quantity); - assertEq(ethPrice, quantity * baseEthPrice); - assertEq(currencyPrice, 0); + assertEq(price.eth, quantity * basePrice.eth); + assertEq(price.currency, 0); } function testProductPrice__MinQuantity() public { @@ -196,23 +192,21 @@ contract NFTDiscountTest is RegistryProductPriceTest { erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + Price memory price = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); - (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, 0, ETH, quantity); - assertEq(ethPrice, quantity * baseEthPrice); - assertEq(currencyPrice, 0); + assertEq(price.eth, quantity * basePrice.eth); + assertEq(price.currency, 0); /// Buyer owns 2 NFTs, minQuantity is 2 nftOne.mint(buyer); /// check product price - (uint256 secondEthPrice, uint256 secondCurrencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + Price memory secondPrice = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); - assertEq(secondEthPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); - assertEq(secondCurrencyPrice, 0); + assertEq(secondPrice.eth, quantity * (basePrice.eth - (basePrice.eth * percentDiscountOne) / 1e4)); + assertEq(secondPrice.currency, 0); } function testProductPrice__MultipleBoughtQuantity() public { @@ -233,13 +227,12 @@ contract NFTDiscountTest is RegistryProductPriceTest { quantity = 6; /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + Price memory price = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); - (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, 0, ETH, quantity); - assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); - assertEq(currencyPrice, 0); + assertEq(price.eth, quantity * (basePrice.eth - (basePrice.eth * percentDiscountOne) / 1e4)); + assertEq(price.currency, 0); } function testConfigureProduct__Edit_Add() public { @@ -260,13 +253,12 @@ contract NFTDiscountTest is RegistryProductPriceTest { nftTwo.mint(buyer); /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + Price memory price = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); - (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, 0, ETH, quantity); - assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountTwo) / 1e4)); - assertEq(currencyPrice, 0); + assertEq(price.eth, quantity * (basePrice.eth - (basePrice.eth * percentDiscountTwo) / 1e4)); + assertEq(price.currency, 0); discountParams = new DiscountParams[](2); @@ -291,11 +283,10 @@ contract NFTDiscountTest is RegistryProductPriceTest { erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); /// check product price - (uint256 secondEthPrice, uint256 secondCurrencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + Price memory secondPrice = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); - assertEq(secondEthPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); - assertEq(secondCurrencyPrice, 0); + assertEq(secondPrice.eth, quantity * (basePrice.eth - (basePrice.eth * percentDiscountOne) / 1e4)); + assertEq(secondPrice.currency, 0); } function testConfigureProduct__Edit_Remove() public { @@ -325,13 +316,12 @@ contract NFTDiscountTest is RegistryProductPriceTest { erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); /// check product price - (uint256 secondEthPrice, uint256 secondCurrencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + Price memory secondPrice = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); - (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, 0, ETH, quantity); - assertEq(secondEthPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); - assertEq(secondCurrencyPrice, 0); + assertEq(secondPrice.eth, quantity * (basePrice.eth - (basePrice.eth * percentDiscountOne) / 1e4)); + assertEq(secondPrice.currency, 0); discountParams = new DiscountParams[](0); @@ -339,10 +329,9 @@ contract NFTDiscountTest is RegistryProductPriceTest { erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + Price memory price = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); - assertEq(ethPrice, quantity * baseEthPrice); - assertEq(currencyPrice, 0); + assertEq(price.eth, quantity * basePrice.eth); + assertEq(price.currency, 0); } } diff --git a/test/pricing/VRGDA/LinearVRGDA.t.sol b/test/pricing/VRGDA/LinearVRGDA.t.sol index 1b57588..a651816 100644 --- a/test/pricing/VRGDA/LinearVRGDA.t.sol +++ b/test/pricing/VRGDA/LinearVRGDA.t.sol @@ -136,17 +136,15 @@ contract LinearVRGDATest is RegistryProductPriceTest { vm.warp(block.timestamp + timeDelta); - (uint256 ethPrice, uint256 currencyPrice) = - vrgda.productPrice(slicerId, productId_, address(0), 1, address(0), ""); + Price memory price = vrgda.productPrice(slicerId, productId_, 0, address(0), 1, address(0), ""); - assertApproxEqAbs(uint256(uint128(targetPriceConstant)), ethPrice, 0.00001e18); - assertEq(currencyPrice, 0); + assertApproxEqAbs(uint256(uint128(targetPriceConstant)), price.eth, 0.00001e18); + assertEq(price.currency, 0); - (uint256 ethPrice2, uint256 currencyPrice2) = - vrgda.productPrice(slicerId, productId_, address(20), 1, address(0), ""); + Price memory price2 = vrgda.productPrice(slicerId, productId_, 0, address(20), 1, address(0), ""); - assertEq(ethPrice2, 0); - assertApproxEqAbs(uint256(uint128(targetPriceConstant)), currencyPrice2, 0.00001e18); + assertEq(price2.eth, 0); + assertApproxEqAbs(uint256(uint128(targetPriceConstant)), price2.currency, 0.00001e18); } function testProductPriceEth() public { @@ -163,11 +161,10 @@ contract LinearVRGDATest is RegistryProductPriceTest { targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), 0, perTimeUnit, min, 1 ); - (uint256 ethPrice, uint256 currencyPrice) = - vrgda.productPrice(slicerId, productId, ethCurrency, 1, address(0), ""); + Price memory price = vrgda.productPrice(slicerId, productId, 0, ethCurrency, 1, address(0), ""); - assertApproxEqAbs(cost, ethPrice, 0.00001e18); - assertEq(currencyPrice, 0); + assertApproxEqAbs(cost, price.eth, 0.00001e18); + assertEq(price.currency, 0); } function testProductPriceErc20() public { @@ -184,11 +181,10 @@ contract LinearVRGDATest is RegistryProductPriceTest { targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), 0, perTimeUnit, min, 1 ); - (uint256 ethPrice, uint256 currencyPrice) = - vrgda.productPrice(slicerId, productId, erc20Currency, 1, address(0), ""); + Price memory price = vrgda.productPrice(slicerId, productId, 0, erc20Currency, 1, address(0), ""); - assertApproxEqAbs(cost, currencyPrice, 0.00001e18); - assertEq(ethPrice, 0); + assertApproxEqAbs(cost, price.currency, 0.00001e18); + assertEq(price.eth, 0); } function testProductPriceMultiple() public { @@ -204,8 +200,8 @@ contract LinearVRGDATest is RegistryProductPriceTest { uint256 costMultiple = vrgda.getAdjustedVRGDAPrice( targetPriceConstant, decayConstant, toDaysWadUnsafe(block.timestamp), 0, perTimeUnit, min, 3 ); - (uint256 ethPrice,) = vrgda.productPrice(slicerId, productId, ethCurrency, 3, address(0), ""); + Price memory price = vrgda.productPrice(slicerId, productId, 0, ethCurrency, 3, address(0), ""); - assertApproxEqAbs(costMultiple, ethPrice, 5e14); + assertApproxEqAbs(costMultiple, price.eth, 5e14); } } diff --git a/test/pricing/VRGDA/LogisticVRGDA.t.sol b/test/pricing/VRGDA/LogisticVRGDA.t.sol index 654e3af..f8af08d 100644 --- a/test/pricing/VRGDA/LogisticVRGDA.t.sol +++ b/test/pricing/VRGDA/LogisticVRGDA.t.sol @@ -173,18 +173,16 @@ contract LogisticVRGDATest is RegistryProductPriceTest { vm.warp(block.timestamp + 10 days); - (uint256 ethPrice, uint256 currencyPrice) = - vrgda.productPrice(slicerId, productIdTest, address(0), 1, address(0), ""); + Price memory price = vrgda.productPrice(slicerId, productIdTest, 0, address(0), 1, address(0), ""); // assertApproxEqAbs(uint256(targetPriceTest), ethPrice, 1e18); - assertNotEq(ethPrice, 0); - assertEq(currencyPrice, 0); + assertNotEq(price.eth, 0); + assertEq(price.currency, 0); - (uint256 ethPrice2, uint256 currencyPrice2) = - vrgda.productPrice(slicerId, productId, address(20), 1, address(0), ""); + Price memory price2 = vrgda.productPrice(slicerId, productId, 0, address(20), 1, address(0), ""); - assertEq(ethPrice2, 0); - assertNotEq(currencyPrice2, 0); + assertEq(price2.eth, 0); + assertNotEq(price2.currency, 0); } function testProductPriceEth() public { @@ -207,11 +205,10 @@ contract LogisticVRGDATest is RegistryProductPriceTest { 1 ); - (uint256 ethPrice, uint256 currencyPrice) = - vrgda.productPrice(slicerId, productId, ethCurrency, 1, address(0), ""); + Price memory price = vrgda.productPrice(slicerId, productId, 0, ethCurrency, 1, address(0), ""); - assertApproxEqAbs(cost, ethPrice, 0.00001e18); - assertEq(currencyPrice, 0); + assertApproxEqAbs(cost, price.eth, 0.00001e18); + assertEq(price.currency, 0); } function testProductPriceErc20() public { @@ -234,11 +231,10 @@ contract LogisticVRGDATest is RegistryProductPriceTest { 1 ); - (uint256 ethPrice, uint256 currencyPrice) = - vrgda.productPrice(slicerId, productId, erc20Currency, 1, address(0), ""); + Price memory price = vrgda.productPrice(slicerId, productId, 0, erc20Currency, 1, address(0), ""); - assertApproxEqAbs(cost, currencyPrice, 0.00001e18); - assertEq(ethPrice, 0); + assertApproxEqAbs(cost, price.currency, 0.00001e18); + assertEq(price.eth, 0); } function testProductPriceMultiple() public { @@ -260,9 +256,9 @@ contract LogisticVRGDATest is RegistryProductPriceTest { min, 3 ); - (uint256 ethPrice,) = vrgda.productPrice(slicerId, productId, ethCurrency, 3, address(0), ""); + Price memory price = vrgda.productPrice(slicerId, productId, 0, ethCurrency, 3, address(0), ""); - assertApproxEqAbs(costMultiple, ethPrice, 5e14); + assertApproxEqAbs(costMultiple, price.eth, 5e14); } function testGetTargetSaleTimeDoesNotRevertEarly() public view { diff --git a/test/pricingActions/FirstForFree/FirstForFree.t.sol b/test/pricingActions/FirstForFree/FirstForFree.t.sol index a4bbfa9..a817d4f 100644 --- a/test/pricingActions/FirstForFree/FirstForFree.t.sol +++ b/test/pricingActions/FirstForFree/FirstForFree.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.30; import {RegistryProductPriceActionTest} from "@test/utils/RegistryProductPriceActionTest.sol"; -import {FirstForFree} from "@/hooks/pricingActions/FirstForFree/FirstForFree.sol"; +import {Price, FirstForFree} from "@/hooks/pricingActions/FirstForFree/FirstForFree.sol"; import {ProductParams} from "@/hooks/pricingActions/FirstForFree/types/ProductParams.sol"; import {TokenCondition, TokenType} from "@/hooks/pricingActions/FirstForFree/types/TokenCondition.sol"; import {ITokenERC1155} from "@/hooks/pricingActions/FirstForFree/utils/ITokenERC1155.sol"; @@ -179,41 +179,40 @@ 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); + Price memory price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, buyer, ""); + assertEq(price.eth, 0); + assertEq(price.currency, 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], 0, address(0), 1, buyer, ""); + assertEq(price.eth, 0); + assertEq(price.currency, 0); // Partial free purchase - (ethPrice, currencyPrice) = firstForFree.productPrice(slicerId, productIds[0], address(0), 2, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, 0); + price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 2, buyer, ""); + assertEq(price.eth, 0); + assertEq(price.currency, 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 + price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 3, buyer, ""); + assertEq(price.eth, 0); + assertEq(price.currency, USDC_PRICE); // 1 paid unit // Purchase after using some free units (simulate 1 purchase made) vm.prank(address(PRODUCTS_MODULE)); - firstForFree.onProductPurchase(slicerId, productIds[0], buyer, 1, "", ""); + firstForFree.onProductPurchase(slicerId, productIds[0], 0, buyer, 1, ""); - (ethPrice, currencyPrice) = firstForFree.productPrice(slicerId, productIds[0], address(0), 2, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, USDC_PRICE); // 1 free, 1 paid + price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 2, buyer, ""); + assertEq(price.eth, 0); + assertEq(price.currency, USDC_PRICE); // 1 free, 1 paid // Purchase after using all free units (simulate 2 total purchases made) vm.prank(address(PRODUCTS_MODULE)); - firstForFree.onProductPurchase(slicerId, productIds[0], buyer, 1, "", ""); + firstForFree.onProductPurchase(slicerId, productIds[0], 0, buyer, 1, ""); - (ethPrice, currencyPrice) = firstForFree.productPrice(slicerId, productIds[0], address(0), 1, buyer, ""); - assertEq(ethPrice, 0); - assertEq(currencyPrice, USDC_PRICE); // All paid + price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, buyer, ""); + assertEq(price.eth, 0); + assertEq(price.currency, USDC_PRICE); // All paid } function testProductPrice_ERC721Condition() public { @@ -239,28 +238,27 @@ 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); + Price memory price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, buyer, ""); + assertEq(price.eth, 0); + assertEq(price.currency, USDC_PRICE); // 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], 0, address(0), 1, buyer, ""); + assertEq(price.eth, 0); + assertEq(price.currency, 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], 0, address(0), 1, buyer, ""); + assertEq(price.eth, 0); + assertEq(price.currency, 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 + price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 3, buyer, ""); + assertEq(price.eth, 0); + assertEq(price.currency, USDC_PRICE); // 2 free, 1 paid } function testProductPrice_ERC1155Condition() public { @@ -291,24 +289,23 @@ 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); + Price memory price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, buyer, ""); + assertEq(price.eth, 0); + assertEq(price.currency, USDC_PRICE); // 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); + price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 2, buyer, ""); + assertEq(price.eth, 0); + assertEq(price.currency, 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], 0, address(0), 1, buyer, ""); + assertEq(price.eth, 0); + assertEq(price.currency, 0); } function testProductPrice_MultipleConditions() public { @@ -341,29 +338,27 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { // 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); + Price memory price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, buyer, ""); + assertEq(price.eth, 0); + assertEq(price.currency, 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], 0, address(0), 1, buyer2, ""); + assertEq(price.eth, 0); + assertEq(price.currency, 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], 0, address(0), 1, buyer3, ""); + assertEq(price.eth, 0); + assertEq(price.currency, 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], 0, address(0), 1, address(0x999), ""); + assertEq(price.eth, 0); + assertEq(price.currency, USDC_PRICE); } function testOnProductPurchase_WithMinting() public { @@ -389,7 +384,7 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { assertEq(mockMintToken.balanceOf(buyer, 5), 0); vm.prank(address(PRODUCTS_MODULE)); - firstForFree.onProductPurchase(slicerId, productIds[0], buyer, 3, "", ""); + firstForFree.onProductPurchase(slicerId, productIds[0], 0, buyer, 3, ""); // Check purchase tracking assertEq(firstForFree.purchases(buyer, slicerId), 3); @@ -398,14 +393,14 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { // Second purchase vm.prank(address(PRODUCTS_MODULE)); - firstForFree.onProductPurchase(slicerId, productIds[0], buyer, 2, "", ""); + firstForFree.onProductPurchase(slicerId, productIds[0], 0, 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], 0, buyer2, 1, ""); assertEq(firstForFree.purchases(buyer2, slicerId), 1); assertEq(mockMintToken.balanceOf(buyer2, 5), 1); @@ -432,7 +427,7 @@ 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], 0, buyer, 2, ""); // Only purchase tracking, no minting assertEq(firstForFree.purchases(buyer, slicerId), 2); @@ -461,23 +456,22 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { // Purchase on slicer 0 vm.prank(address(PRODUCTS_MODULE)); - firstForFree.onProductPurchase(0, productIds[0], buyer, 2, "", ""); + firstForFree.onProductPurchase(0, productIds[0], 0, buyer, 2, ""); // Purchase on slicer 1 vm.prank(address(PRODUCTS_MODULE)); - firstForFree.onProductPurchase(1, productIds[0], buyer, 1, "", ""); + firstForFree.onProductPurchase(1, productIds[0], 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 + Price memory price = firstForFree.productPrice(0, productIds[0], 0, address(0), 1, buyer, ""); + assertEq(price.currency, 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 + price = firstForFree.productPrice(1, productIds[0], 0, address(0), 1, buyer, ""); + assertEq(price.currency, 0); // Still has free units on slicer 1 } function testEdgeCases() public { @@ -497,15 +491,14 @@ 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); + Price memory price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, buyer, ""); + assertEq(price.eth, 0); + assertEq(price.currency, USDC_PRICE); // 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], 0, address(0), 0, buyer, ""); + assertEq(price.eth, 0); + assertEq(price.currency, 0); } function testConfigureProduct_UpdateExisting() public { 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/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..3f45593 100644 --- a/test/utils/RegistryProductActionTest.sol +++ b/test/utils/RegistryProductActionTest.sol @@ -12,6 +12,6 @@ abstract contract RegistryProductActionTest is HookRegistryTest { 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..118724e 100644 --- a/test/utils/RegistryProductPriceActionTest.sol +++ b/test/utils/RegistryProductPriceActionTest.sol @@ -19,6 +19,6 @@ abstract contract RegistryProductPriceActionTest is HookRegistryTest { 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/mocks/MockProductsModule.sol b/test/utils/mocks/MockProductsModule.sol index 0b8a4a9..a7b1f47 100644 --- a/test/utils/mocks/MockProductsModule.sol +++ b/test/utils/mocks/MockProductsModule.sol @@ -2,6 +2,7 @@ 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 @@ -11,16 +12,12 @@ contract MockProductsModule is isAllowed = account == vm.addr(uint256(keccak256(abi.encodePacked("productOwner")))); } - function basePrice(uint256, uint256, address, uint256) - external - pure - returns (uint256 ethPrice, uint256 currencyPrice) - { - ethPrice = 1e16; - currencyPrice = 100e18; + function basePrice(uint256, uint256, uint256, address, uint256) external pure returns (Price memory price) { + price.eth = 1e16; + price.currency = 100e18; } - 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; } From 94e04a9ec32dab3810b4c134d618be0abfcd2344 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 14 Sep 2025 18:49:36 +0200 Subject: [PATCH 03/30] access control init --- foundry.toml | 2 ++ test/utils/HookRegistryTest.sol | 5 ++++- test/utils/mocks/MockProductsModule.sol | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index 0f37899..02339f1 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,6 +10,7 @@ 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/", + "solmate-6.8.0/=dependencies/solmate-6.8.0/", "@erc721a/=dependencies/erc721a-4.3.0/contracts/", "forge-std/=dependencies/forge-std-1.9.7/src/", "@test/=test/", @@ -49,6 +50,7 @@ forge-std = "1.9.7" "@openzeppelin-contracts" = "4.8.0" "@openzeppelin-contracts-upgradeable" = "4.8.0" erc721a = "4.3.0" +solmate = "6.8.0" [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"] diff --git a/test/utils/HookRegistryTest.sol b/test/utils/HookRegistryTest.sol index f381db1..ed787e7 100644 --- a/test/utils/HookRegistryTest.sol +++ b/test/utils/HookRegistryTest.sol @@ -6,6 +6,8 @@ import {IHookRegistry} from "@/utils/RegistryProductPrice.sol"; import {MockProductsModule} from "./mocks/MockProductsModule.sol"; import {SliceContext} from "@/utils/RegistryProductAction.sol"; +bytes32 constant PRODUCT_MANAGER_ROLE = bytes32(uint256(0x04)); + abstract contract HookRegistryTest is HookTest { function testParamsSchema() public view { string memory schema = IHookRegistry(hook).paramsSchema(); @@ -13,7 +15,8 @@ abstract contract HookRegistryTest is HookTest { } function testConfigureProduct_AccessControl() public { - vm.expectRevert(abi.encodeWithSelector(SliceContext.NotProductOwner.selector)); + vm.expectRevert(abi.encodeWithSelector(SliceContext.NotAuthorized.selector, PRODUCT_MANAGER_ROLE)); + IHookRegistry(hook).configureProduct(0, 0, ""); } diff --git a/test/utils/mocks/MockProductsModule.sol b/test/utils/mocks/MockProductsModule.sol index a7b1f47..6935efa 100644 --- a/test/utils/mocks/MockProductsModule.sol +++ b/test/utils/mocks/MockProductsModule.sol @@ -8,6 +8,10 @@ import {Test} from "forge-std/Test.sol"; contract MockProductsModule is Test // , IProductsModule { + function hasRole(uint256, bytes32, address account) public pure returns (bool _hasRole) { + _hasRole = account == vm.addr(uint256(keccak256(abi.encodePacked("productOwner")))); + } + function isProductOwner(uint256, uint256, address account) external pure returns (bool isAllowed) { isAllowed = account == vm.addr(uint256(keccak256(abi.encodePacked("productOwner")))); } From 213f372625e801444ed714c4ffb78ee20bead899 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 14 Sep 2025 19:11:42 +0200 Subject: [PATCH 04/30] wip --- foundry.toml | 4 ++-- soldeer.lock | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/foundry.toml b/foundry.toml index 02339f1..a8f247f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,7 +10,7 @@ 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/", - "solmate-6.8.0/=dependencies/solmate-6.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/", @@ -50,7 +50,7 @@ forge-std = "1.9.7" "@openzeppelin-contracts" = "4.8.0" "@openzeppelin-contracts-upgradeable" = "4.8.0" erc721a = "4.3.0" -solmate = "6.8.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"] 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" From 5119cc5aa9d864de0180bf5edc4ebd94c1295025 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 15 Sep 2025 01:52:44 +0200 Subject: [PATCH 05/30] access control final draft --- test/utils/HookRegistryTest.sol | 7 ++++++- test/utils/mocks/MockProductsModule.sol | 8 ++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/test/utils/HookRegistryTest.sol b/test/utils/HookRegistryTest.sol index ed787e7..54723b7 100644 --- a/test/utils/HookRegistryTest.sol +++ b/test/utils/HookRegistryTest.sol @@ -15,7 +15,12 @@ abstract contract HookRegistryTest is HookTest { } function testConfigureProduct_AccessControl() public { - vm.expectRevert(abi.encodeWithSelector(SliceContext.NotAuthorized.selector, PRODUCT_MANAGER_ROLE)); + vm.expectRevert( + abi.encodeWithSelector( + SliceContext.NotAuthorized.selector, + bytes32(uint256(1 << 1 | 1 << 4)) /* Role.ADMIN.getMask() | Role.PRODUCT_MANAGER.getMask() */ + ) + ); IHookRegistry(hook).configureProduct(0, 0, ""); } diff --git a/test/utils/mocks/MockProductsModule.sol b/test/utils/mocks/MockProductsModule.sol index 6935efa..eecf4a3 100644 --- a/test/utils/mocks/MockProductsModule.sol +++ b/test/utils/mocks/MockProductsModule.sol @@ -8,8 +8,12 @@ import {Test} from "forge-std/Test.sol"; contract MockProductsModule is Test // , IProductsModule { - function hasRole(uint256, bytes32, address account) public pure returns (bool _hasRole) { - _hasRole = account == vm.addr(uint256(keccak256(abi.encodePacked("productOwner")))); + error NotAuthorized(bytes32 requiredRolesMask); + + function checkRoles(uint256, bytes32, address account) public pure { + if (account != vm.addr(uint256(keccak256(abi.encodePacked("productOwner"))))) { + revert NotAuthorized(bytes32(uint256(1 << 1 | 1 << 4))); + } } function isProductOwner(uint256, uint256, address account) external pure returns (bool isAllowed) { From 211d10e5266277fa5605da1d8e8c5d755db9988f Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 15 Sep 2025 02:26:01 +0200 Subject: [PATCH 06/30] cleanup --- test/utils/mocks/MockProductsModule.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/mocks/MockProductsModule.sol b/test/utils/mocks/MockProductsModule.sol index eecf4a3..e89308e 100644 --- a/test/utils/mocks/MockProductsModule.sol +++ b/test/utils/mocks/MockProductsModule.sol @@ -8,7 +8,7 @@ import {Test} from "forge-std/Test.sol"; contract MockProductsModule is Test // , IProductsModule { - error NotAuthorized(bytes32 requiredRolesMask); + error NotAuthorized(bytes32 rolesMask); function checkRoles(uint256, bytes32, address account) public pure { if (account != vm.addr(uint256(keccak256(abi.encodePacked("productOwner"))))) { From a6eb47e5e11f38c0594a24dfe3a439f8ff3c0a92 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 17 Sep 2025 02:32:28 +0200 Subject: [PATCH 07/30] multi-actions support: init --- src/hooks/actions/Allowlisted/Allowlisted.sol | 18 ++++++++---------- .../LinearVRGDAPrices/LinearVRGDAPrices.sol | 10 +++++----- .../LogisticVRGDAPrices.sol | 10 +++++----- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/hooks/actions/Allowlisted/Allowlisted.sol b/src/hooks/actions/Allowlisted/Allowlisted.sol index 83e3187..198a53c 100644 --- a/src/hooks/actions/Allowlisted/Allowlisted.sol +++ b/src/hooks/actions/Allowlisted/Allowlisted.sol @@ -36,16 +36,14 @@ contract Allowlisted is RegistryProductAction { * @inheritdoc IProductAction * @dev Checks if the account is in the allowlist. */ - function isPurchaseAllowed( - uint256 slicerId, - uint256 productId, - uint256, - address buyer, - uint256, - bytes memory buyerCustomData - ) public view override returns (bool isAllowed) { - // Get Merkle proof from buyerCustomData - bytes32[] memory proof = abi.decode(buyerCustomData, (bytes32[])); + function isPurchaseAllowed(uint256 slicerId, uint256 productId, uint256, address buyer, uint256, bytes memory data) + public + view + override + returns (bool isAllowed) + { + // Get Merkle proof from data + bytes32[] memory proof = abi.decode(data, (bytes32[])); uint256 leafValue = uint256(uint160(buyer)); diff --git a/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol index 84150af..08945f8 100644 --- a/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol @@ -44,14 +44,14 @@ contract LinearVRGDAPrices is VRGDAPrices { /// Get product availability and isInfinite /// @dev available units is a uint32 - (uint256 availableUnits, bool isInfinite) = PRODUCTS_MODULE.stockUnits(slicerId, productId, 0); + (uint256 stockUnits, bool isInfinite) = PRODUCTS_MODULE.stockUnits(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].startUnits = uint32(stockUnits); _productParams[slicerId][productId].decayConstant = int184(decayConstant); // Set currency params @@ -83,10 +83,10 @@ contract LinearVRGDAPrices is VRGDAPrices { require(productParams.startTime != 0, "PRODUCT_UNSET"); // Get available units - (uint256 availableUnits,) = PRODUCTS_MODULE.stockUnits(slicerId, productId, variantId); + (uint256 stockUnits,) = PRODUCTS_MODULE.stockUnits(slicerId, productId, variantId); - // Calculate sold units from availableUnits - uint256 soldUnits = productParams.startUnits - availableUnits; + // Calculate sold units from stockUnits + uint256 soldUnits = productParams.startUnits - stockUnits; // Set ethPrice or currencyPrice based on chosen currency if (currency == address(0)) { diff --git a/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol b/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol index c2175ea..28c8c45 100644 --- a/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol @@ -52,14 +52,14 @@ contract LogisticVRGDAPrices is VRGDAPrices { require(decayConstant >= type(int184).min, "MIN_DECAY_CONSTANT_EXCEEDED"); // Get product availability and isInfinite - (uint256 availableUnits, bool isInfinite) = PRODUCTS_MODULE.stockUnits(slicerId, productId, 0); + (uint256 stockUnits, bool isInfinite) = PRODUCTS_MODULE.stockUnits(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].startUnits = uint32(stockUnits); _productParams[slicerId][productId].decayConstant = int184(decayConstant); // Set currency params @@ -91,7 +91,7 @@ contract LogisticVRGDAPrices is VRGDAPrices { require(productParams.startTime != 0, "PRODUCT_UNSET"); // Get available units - (uint256 availableUnits,) = PRODUCTS_MODULE.stockUnits(slicerId, productId, variantId); + (uint256 stockUnits,) = PRODUCTS_MODULE.stockUnits(slicerId, productId, variantId); // Set ethPrice or currencyPrice based on chosen currency if (currency == address(0)) { @@ -100,7 +100,7 @@ contract LogisticVRGDAPrices is VRGDAPrices { productParams.decayConstant, toDaysWadUnsafe(block.timestamp - productParams.startTime), toWadUnsafe(productParams.startUnits + 1), - productParams.startUnits - availableUnits, + productParams.startUnits - stockUnits, pricingParams.timeScale, pricingParams.min, quantity @@ -111,7 +111,7 @@ contract LogisticVRGDAPrices is VRGDAPrices { productParams.decayConstant, toDaysWadUnsafe(block.timestamp - productParams.startTime), toWadUnsafe(productParams.startUnits + 1), - productParams.startUnits - availableUnits, + productParams.startUnits - stockUnits, pricingParams.timeScale, pricingParams.min, quantity From faea2fde7801e1082d7f6fddd78c2f7622c15ac0 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 22 Sep 2025 21:57:15 +0200 Subject: [PATCH 08/30] multi-pricing init --- .../NFTDiscount/NFTDiscount.sol | 20 ++--- .../pricing/TieredDiscount/TieredDiscount.sol | 23 ++--- .../LinearVRGDAPrices/LinearVRGDAPrices.sol | 80 +++++++---------- .../LogisticVRGDAPrices.sol | 85 +++++++------------ ...ticProductParams.sol => LinearProduct.sol} | 10 +-- .../pricing/VRGDA/types/LinearVRGDAParams.sol | 12 --- ...rProductParams.sol => LogisticProduct.sol} | 10 +-- .../VRGDA/types/LogisticVRGDAParams.sol | 13 --- .../FirstForFree/FirstForFree.sol | 10 ++- 9 files changed, 91 insertions(+), 172 deletions(-) rename src/hooks/pricing/VRGDA/types/{LogisticProductParams.sol => LinearProduct.sol} (57%) delete mode 100644 src/hooks/pricing/VRGDA/types/LinearVRGDAParams.sol rename src/hooks/pricing/VRGDA/types/{LinearProductParams.sol => LogisticProduct.sol} (62%) delete mode 100644 src/hooks/pricing/VRGDA/types/LogisticVRGDAParams.sol diff --git a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol index 560cd2e..200c512 100644 --- a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol @@ -80,22 +80,16 @@ contract NFTDiscount is TieredDiscount { uint256, uint256, uint256, - address currency, + address, uint256 quantity, address buyer, bytes memory, - uint256 basePrice, + uint256 basePriceAmount, DiscountParams[] memory discountParams - ) internal view virtual override returns (Price memory price) { + ) internal view virtual override returns (uint256 price) { uint256 discount = _getHighestDiscount(discountParams, buyer); - uint256 amount = discount != 0 ? _getDiscountedPrice(basePrice, discount, quantity) : quantity * basePrice; - - if (currency == address(0)) { - price.eth = amount; - } else { - price.currency = amount; - } + price = discount != 0 ? _getDiscountedPrice(basePriceAmount, discount, quantity) : quantity * basePriceAmount; } /*////////////////////////////////////////////////////////////// @@ -156,13 +150,13 @@ 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, uint256 quantity) internal pure virtual @@ -174,6 +168,6 @@ contract NFTDiscount is TieredDiscount { k = 1e4 - discount; } - price = (basePrice * k * quantity) / 1e4; + price = (basePriceAmount * k * quantity) / 1e4; } } diff --git a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol index 761f38c..767c240 100644 --- a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol @@ -45,25 +45,14 @@ abstract contract TieredDiscount is RegistryProductPrice { uint256 productId, uint256 variantId, address currency, + uint256 basePrice, uint256 quantity, address buyer, bytes memory data - ) public view override returns (Price memory) { - Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, currency, quantity); - + ) public view override returns (uint256) { DiscountParams[] memory discountParams = discounts[slicerId][productId]; - return _productPrice( - slicerId, - productId, - variantId, - currency, - quantity, - buyer, - data, - currency == address(0) ? basePrice.eth : basePrice.currency, - discountParams - ); + return _productPrice(slicerId, productId, variantId, currency, quantity, buyer, data, basePrice, discountParams); } /*////////////////////////////////////////////////////////////// @@ -79,10 +68,10 @@ abstract contract TieredDiscount is RegistryProductPrice { * @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, @@ -94,5 +83,5 @@ abstract contract TieredDiscount is RegistryProductPrice { bytes memory data, uint256 basePrice, DiscountParams[] memory discountParams - ) internal view virtual returns (Price memory); + ) internal view virtual returns (uint256); } diff --git a/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol index 08945f8..3f2a057 100644 --- a/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol @@ -3,8 +3,7 @@ pragma solidity ^0.8.30; import {HookRegistry, IProductPrice, IHookRegistry, 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"; import {Price} from "slice/types/Price.sol"; @@ -17,7 +16,7 @@ contract LinearVRGDAPrices is VRGDAPrices { //////////////////////////////////////////////////////////////*/ // Mapping from slicerId to productId to ProductParams - mapping(uint256 => mapping(uint256 => LinearProductParams)) private _productParams; + mapping(uint256 => mapping(uint256 => LinearProduct)) private _productParams; /*////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -34,8 +33,7 @@ contract LinearVRGDAPrices is VRGDAPrices { * @notice Set LinearVRGDAParams for a product. */ function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { - (LinearVRGDAParams[] memory linearParams, int256 priceDecayPercent) = - abi.decode(params, (LinearVRGDAParams[], int256)); + (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. @@ -43,25 +41,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 stockUnits, bool isInfinite) = PRODUCTS_MODULE.stockUnits(slicerId, productId, 0); + (uint256 stockUnits_, bool isInfinite) = stockUnits(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(stockUnits); - _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] = LinearProduct({ + startTime: uint40(block.timestamp), + startUnits: uint32(stockUnits_), + decayConstant: int184(decayConstant), + min: min, + perTimeUnit: perTimeUnit + }); } /** @@ -72,52 +64,38 @@ contract LinearVRGDAPrices is VRGDAPrices { uint256 productId, uint256 variantId, address currency, + uint256, uint256 quantity, address, bytes memory - ) public view override returns (Price memory price) { - // 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]; require(productParams.startTime != 0, "PRODUCT_UNSET"); - // Get available units - (uint256 stockUnits,) = PRODUCTS_MODULE.stockUnits(slicerId, productId, variantId); + // Get available units and base price + (uint256 stockUnits_,) = stockUnits(slicerId, productId, variantId); + uint256 basePrice_ = basePrice(slicerId, productId, variantId, currency, 1); // Calculate sold units from stockUnits - uint256 soldUnits = productParams.startUnits - stockUnits; - - // Set ethPrice or currencyPrice based on chosen currency - if (currency == address(0)) { - price.eth = getAdjustedVRGDAPrice( - pricingParams.targetPrice, - productParams.decayConstant, - toDaysWadUnsafe(block.timestamp - productParams.startTime), - soldUnits, - pricingParams.perTimeUnit, - pricingParams.min, - quantity - ); - } else { - price.currency = getAdjustedVRGDAPrice( - pricingParams.targetPrice, - productParams.decayConstant, - toDaysWadUnsafe(block.timestamp - productParams.startTime), - soldUnits, - pricingParams.perTimeUnit, - pricingParams.min, - quantity - ); - } + uint256 soldUnits = productParams.startUnits - stockUnits_; + + price = getAdjustedVRGDAPrice( + int256(basePrice_), + productParams.decayConstant, + toDaysWadUnsafe(block.timestamp - productParams.startTime), + soldUnits, + productParams.perTimeUnit, + basePrice_ * productParams.min / 1e5, + quantity + ); } /** * @inheritdoc IHookRegistry */ function paramsSchema() external pure override returns (string memory) { - return - "(address currency,int128 targetPrice,uint128 min,int256 perTimeUnit)[] linearParams,int256 priceDecayPercent"; + 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 28c8c45..111d591 100644 --- a/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol @@ -12,8 +12,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"; import {Price} from "slice/types/Price.sol"; @@ -25,8 +24,8 @@ 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 => mapping(uint256 => LogisticProduct)) private _productParams; /*////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -43,8 +42,7 @@ contract LogisticVRGDAPrices is VRGDAPrices { * @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)); + (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. @@ -52,24 +50,19 @@ contract LogisticVRGDAPrices is VRGDAPrices { require(decayConstant >= type(int184).min, "MIN_DECAY_CONSTANT_EXCEEDED"); // Get product availability and isInfinite - (uint256 stockUnits, bool isInfinite) = PRODUCTS_MODULE.stockUnits(slicerId, productId, 0); + (uint256 stockUnits_, bool isInfinite) = stockUnits(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(stockUnits); - _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] = LogisticProduct({ + startTime: uint40(block.timestamp), + startUnits: uint32(stockUnits_), + decayConstant: int184(decayConstant), + min: min, + timeScale: timescale + }); } /** @@ -80,51 +73,39 @@ contract LogisticVRGDAPrices is VRGDAPrices { uint256 productId, uint256 variantId, address currency, + uint256, uint256 quantity, address, bytes memory - ) public view override returns (Price memory price) { - // 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]; require(productParams.startTime != 0, "PRODUCT_UNSET"); - // Get available units - (uint256 stockUnits,) = PRODUCTS_MODULE.stockUnits(slicerId, productId, variantId); - - // Set ethPrice or currencyPrice based on chosen currency - if (currency == address(0)) { - price.eth = getAdjustedVRGDALogisticPrice( - pricingParams.targetPrice, - productParams.decayConstant, - toDaysWadUnsafe(block.timestamp - productParams.startTime), - toWadUnsafe(productParams.startUnits + 1), - productParams.startUnits - stockUnits, - pricingParams.timeScale, - pricingParams.min, - quantity - ); - } else { - price.currency = getAdjustedVRGDALogisticPrice( - pricingParams.targetPrice, - productParams.decayConstant, - toDaysWadUnsafe(block.timestamp - productParams.startTime), - toWadUnsafe(productParams.startUnits + 1), - productParams.startUnits - stockUnits, - pricingParams.timeScale, - pricingParams.min, - quantity - ); - } + // Get available units and base price + (uint256 stockUnits_,) = stockUnits(slicerId, productId, variantId); + uint256 basePrice_ = basePrice(slicerId, productId, variantId, currency, 1); + + // Calculate sold units from stockUnits + uint256 soldUnits = productParams.startUnits - stockUnits_; + + price = getAdjustedVRGDALogisticPrice( + int256(basePrice_), + productParams.decayConstant, + toDaysWadUnsafe(block.timestamp - productParams.startTime), + toWadUnsafe(productParams.startUnits + 1), + soldUnits, + productParams.timeScale, + basePrice_ * productParams.min / 1e5, + quantity + ); } /** * @inheritdoc IHookRegistry */ function paramsSchema() external pure override returns (string memory) { - return - "(address currency,int128 targetPrice,uint128 min,int256 timeScale)[] logisticParams,int256 priceDecayPercent"; + 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 3b4046d..e8b0974 100644 --- a/src/hooks/pricingActions/FirstForFree/FirstForFree.sol +++ b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol @@ -48,10 +48,11 @@ contract FirstForFree is RegistryProductPriceAction { uint256 productId, uint256, address, + uint256, uint256 quantity, address buyer, bytes memory - ) public view override returns (Price memory price) { + ) public view override returns (uint256) { ProductParams memory productParams = usdcPrices[slicerId][productId]; if (_isEligible(buyer, productParams.eligibleTokens)) { @@ -59,16 +60,17 @@ contract FirstForFree is RegistryProductPriceAction { if (totalPurchases < productParams.freeUnits) { unchecked { uint256 freeUnitsLeft = productParams.freeUnits - totalPurchases; + if (quantity <= freeUnitsLeft) { - return (Price(0, 0)); + return 0; } else { - return (Price(0, usdcPrices[slicerId][productId].usdcPrice * (quantity - freeUnitsLeft))); + return usdcPrices[slicerId][productId].usdcPrice * (quantity - freeUnitsLeft); } } } } - return (Price(0, usdcPrices[slicerId][productId].usdcPrice * quantity)); + return usdcPrices[slicerId][productId].usdcPrice * quantity; } /** From a1ec6fbdb55d87264d5879bff1a6bc47cf0c33c8 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Thu, 25 Sep 2025 00:43:54 +0200 Subject: [PATCH 09/30] contracts wip --- test/utils/HookRegistryTest.sol | 2 +- test/utils/mocks/MockProductsModule.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/utils/HookRegistryTest.sol b/test/utils/HookRegistryTest.sol index 54723b7..0085e77 100644 --- a/test/utils/HookRegistryTest.sol +++ b/test/utils/HookRegistryTest.sol @@ -18,7 +18,7 @@ abstract contract HookRegistryTest is HookTest { vm.expectRevert( abi.encodeWithSelector( SliceContext.NotAuthorized.selector, - bytes32(uint256(1 << 1 | 1 << 4)) /* Role.ADMIN.getMask() | Role.PRODUCT_MANAGER.getMask() */ + bytes32(uint256(1 << 1 | 1 << 3)) /* Role.ADMIN.getMask() | Role.PRODUCT_MANAGER.getMask() */ ) ); diff --git a/test/utils/mocks/MockProductsModule.sol b/test/utils/mocks/MockProductsModule.sol index e89308e..923ba7f 100644 --- a/test/utils/mocks/MockProductsModule.sol +++ b/test/utils/mocks/MockProductsModule.sol @@ -12,7 +12,7 @@ contract MockProductsModule is function checkRoles(uint256, bytes32, address account) public pure { if (account != vm.addr(uint256(keccak256(abi.encodePacked("productOwner"))))) { - revert NotAuthorized(bytes32(uint256(1 << 1 | 1 << 4))); + revert NotAuthorized(bytes32(uint256(1 << 1 | 1 << 3))); } } From bc9b0ccbff8519ee92b244d67aa732d31a079a6c Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 28 Sep 2025 19:56:37 +0200 Subject: [PATCH 10/30] hook versioning --- src/interfaces/{IProductPrice.sol => IProductActionV1.sol} | 2 +- src/interfaces/{IProductAction.sol => IProductPriceV1.sol} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/interfaces/{IProductPrice.sol => IProductActionV1.sol} (51%) rename src/interfaces/{IProductAction.sol => IProductPriceV1.sol} (52%) diff --git a/src/interfaces/IProductPrice.sol b/src/interfaces/IProductActionV1.sol similarity index 51% rename from src/interfaces/IProductPrice.sol rename to src/interfaces/IProductActionV1.sol index fb9c752..796f503 100644 --- a/src/interfaces/IProductPrice.sol +++ b/src/interfaces/IProductActionV1.sol @@ -1,4 +1,4 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import "slice/interfaces/hooks/IProductPrice.sol"; +import "slice/interfaces/hooks/IProductActionV1.sol"; diff --git a/src/interfaces/IProductAction.sol b/src/interfaces/IProductPriceV1.sol similarity index 52% rename from src/interfaces/IProductAction.sol rename to src/interfaces/IProductPriceV1.sol index 1fd0423..81a8767 100644 --- a/src/interfaces/IProductAction.sol +++ b/src/interfaces/IProductPriceV1.sol @@ -1,4 +1,4 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import "slice/interfaces/hooks/IProductAction.sol"; +import "slice/interfaces/hooks/IProductPriceV1.sol"; From e07e464be4a860276b583808e6716a64469ef37c Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 29 Sep 2025 02:44:31 +0200 Subject: [PATCH 11/30] fix tests --- script/ScriptUtils.sol | 4 +- src/hooks/actions/Allowlisted/Allowlisted.sol | 28 +- src/hooks/actions/ERC20Gated/ERC20Gated.sol | 30 ++- src/hooks/actions/ERC20Mint/ERC20Mint.sol | 35 +-- src/hooks/actions/ERC721Mint/ERC721Mint.sol | 18 +- src/hooks/actions/NFTGated/NFTGated.sol | 32 ++- src/hooks/actions/README.md | 2 +- src/hooks/pricing/README.md | 2 +- .../NFTDiscount/NFTDiscount.sol | 20 +- .../pricing/TieredDiscount/TieredDiscount.sol | 5 +- .../LinearVRGDAPrices/LinearVRGDAPrices.sol | 22 +- .../LogisticVRGDAPrices.sol | 20 +- .../FirstForFree/FirstForFree.sol | 69 +++-- .../FirstForFree/types/ProductParams.sol | 1 - src/hooks/pricingActions/README.md | 2 +- ...IProductPriceV1.sol => IProductAction.sol} | 2 +- ...IProductActionV1.sol => IProductPrice.sol} | 2 +- test/actions/Allowlisted/Allowlisted.t.sol | 13 +- test/actions/ERC20Gated/ERC20Gated.t.sol | 31 +-- .../ERC20Gated/mocks/MockERC20Gated.sol | 4 +- test/actions/ERC20Mint/ERC20Mint.t.sol | 41 ++- test/actions/ERC721Mint/ERC721Mint.t.sol | 62 +++-- test/actions/NFTGated/NFTGated.t.sol | 45 ++-- test/actions/NFTGated/mocks/MockNFTGated.sol | 4 +- test/pricing/TieredDiscount/NFTDiscount.t.sol | 143 +++++----- test/pricing/VRGDA/LinearVRGDA.t.sol | 82 +++--- test/pricing/VRGDA/LogisticVRGDA.t.sol | 84 +++--- .../correctness/LinearVRGDACorrectness.t.sol | 14 +- .../FirstForFree/FirstForFree.t.sol | 244 ++++++++---------- test/utils/HookRegistryTest.sol | 6 +- test/utils/mocks/MockProductsModule.sol | 25 +- 31 files changed, 578 insertions(+), 514 deletions(-) rename src/interfaces/{IProductPriceV1.sol => IProductAction.sol} (52%) rename src/interfaces/{IProductActionV1.sol => IProductPrice.sol} (51%) diff --git a/script/ScriptUtils.sol b/script/ScriptUtils.sol index b65a1d5..82f8294 100644 --- a/script/ScriptUtils.sol +++ b/script/ScriptUtils.sol @@ -213,7 +213,7 @@ abstract contract SetUpContractsList is Script { 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()); + vm.serializeString("0", "paramsSchema", IHookRegistry(transaction.contractAddress).configureProductSchema()); json[0] = vm.serializeBytes32("0", "transactionHash", transaction.hash); for (uint256 i = 0; i < existingContractAddresses.length; i++) { @@ -229,7 +229,7 @@ abstract contract SetUpContractsList is Script { json = new string[](1); vm.serializeAddress("0", "address", transaction.contractAddress); vm.serializeUint("0", "blockNumber", receipt.blockNumber); - vm.serializeString("0", "paramsSchema", IHookRegistry(transaction.contractAddress).paramsSchema()); + vm.serializeString("0", "paramsSchema", IHookRegistry(transaction.contractAddress).configureProductSchema()); json[0] = vm.serializeBytes32("0", "transactionHash", transaction.hash); } } diff --git a/src/hooks/actions/Allowlisted/Allowlisted.sol b/src/hooks/actions/Allowlisted/Allowlisted.sol index 198a53c..acf1dfc 100644 --- a/src/hooks/actions/Allowlisted/Allowlisted.sol +++ b/src/hooks/actions/Allowlisted/Allowlisted.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 @@ -36,12 +37,14 @@ contract Allowlisted is RegistryProductAction { * @inheritdoc IProductAction * @dev Checks if the account is in the allowlist. */ - function isPurchaseAllowed(uint256 slicerId, uint256 productId, uint256, address buyer, uint256, bytes memory data) - public - view - override - returns (bool isAllowed) - { + function isPurchaseAllowed( + uint256 slicerId, + uint256 productId, + uint256 variantId, + address buyer, + uint256, + bytes memory data + ) public view override returns (bool isAllowed) { // Get Merkle proof from data bytes32[] memory proof = abi.decode(data, (bytes32[])); @@ -55,7 +58,7 @@ 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); @@ -65,15 +68,18 @@ contract Allowlisted is RegistryProductAction { * @inheritdoc HookRegistry * @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 */ - function paramsSchema() external pure override returns (string memory) { + function configureProductSchema() external 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 30b8365..b05d06e 100644 --- a/src/hooks/actions/ERC20Gated/ERC20Gated.sol +++ b/src/hooks/actions/ERC20Gated/ERC20Gated.sol @@ -20,7 +20,8 @@ contract ERC20Gated is RegistryProductAction { 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; /*////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -36,13 +37,15 @@ contract ERC20Gated is RegistryProductAction { * @inheritdoc IProductAction * @dev Checks if `account` owns the required amount of all ERC20 tokens. */ - function isPurchaseAllowed(uint256 slicerId, uint256 productId, uint256, address buyer, uint256, bytes memory) - public - view - override - returns (bool) - { - ERC20Gate[] memory gates = tokenGates[slicerId][productId]; + 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]; for (uint256 i = 0; i < gates.length; i++) { ERC20Gate memory gate = gates[i]; @@ -59,20 +62,23 @@ contract ERC20Gated is RegistryProductAction { * @inheritdoc HookRegistry * @dev Sets the ERC20 gates 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 + { (ERC20Gate[] memory gates) = abi.decode(params, (ERC20Gate[])); - delete tokenGates[slicerId][productId]; + delete tokenGates[slicerId][productId][variantId]; for (uint256 i = 0; i < gates.length; i++) { - tokenGates[slicerId][productId].push(gates[i]); + tokenGates[slicerId][productId][variantId].push(gates[i]); } } /** * @inheritdoc IHookRegistry */ - function paramsSchema() external pure override returns (string memory) { + function configureProductSchema() external pure override returns (string memory) { return "(address erc20,uint256 amount)[] erc20Gates"; } } diff --git a/src/hooks/actions/ERC20Mint/ERC20Mint.sol b/src/hooks/actions/ERC20Mint/ERC20Mint.sol index 8f7a1bc..7909f12 100644 --- a/src/hooks/actions/ERC20Mint/ERC20Mint.sol +++ b/src/hooks/actions/ERC20Mint/ERC20Mint.sol @@ -28,7 +28,8 @@ 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; /*////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -44,14 +45,15 @@ contract ERC20Mint is RegistryProductAction { * @inheritdoc IProductAction * @dev If `revertOnMaxSupplyReached` is set to true, returns false when max supply is exceeded. */ - function isPurchaseAllowed(uint256 slicerId, uint256 productId, uint256, address, uint256 quantity, bytes memory) - public - view - virtual - override - returns (bool isAllowed) - { - ERC20Data memory tokenData_ = tokenData[slicerId][productId]; + function isPurchaseAllowed( + uint256 slicerId, + uint256 productId, + uint256 variantId, + address, + uint256 quantity, + bytes memory + ) public view virtual override returns (bool isAllowed) { + ERC20Data memory tokenData_ = tokenData[slicerId][productId][variantId]; if (tokenData_.revertOnMaxSupplyReached) { return @@ -69,12 +71,12 @@ contract ERC20Mint is RegistryProductAction { function _onProductPurchase( uint256 slicerId, uint256 productId, - uint256, + uint256 variantId, address buyer, uint256 quantity, bytes memory ) internal override { - ERC20Data memory tokenData_ = tokenData[slicerId][productId]; + ERC20Data memory tokenData_ = tokenData[slicerId][productId][variantId]; uint256 tokensToMint = quantity * tokenData_.tokensPerUnit; @@ -90,7 +92,10 @@ contract ERC20Mint is RegistryProductAction { * @inheritdoc HookRegistry * @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 + { ( string memory name, string memory symbol, @@ -103,7 +108,7 @@ contract ERC20Mint is RegistryProductAction { if (tokensPerUnit == 0) revert InvalidTokensPerUnit(); - ERC20Mint_BaseToken token = tokenData[slicerId][productId].token; + ERC20Mint_BaseToken token = tokenData[slicerId][productId][variantId].token; if (address(token) == address(0)) { token = new ERC20Mint_BaseToken(name, symbol, maxSupply); @@ -114,13 +119,13 @@ contract ERC20Mint is RegistryProductAction { token.setMaxSupply(maxSupply); } - tokenData[slicerId][productId] = ERC20Data(token, revertOnMaxSupplyReached, tokensPerUnit); + tokenData[slicerId][productId][variantId] = ERC20Data(token, revertOnMaxSupplyReached, tokensPerUnit); } /** * @inheritdoc IHookRegistry */ - function paramsSchema() external pure override returns (string memory) { + function configureProductSchema() external pure override returns (string memory) { return "string name,string symbol,uint256 premintAmount,address premintReceiver,bool revertOnMaxSupplyReached,uint256 maxSupply,uint256 tokensPerUnit"; } diff --git a/src/hooks/actions/ERC721Mint/ERC721Mint.sol b/src/hooks/actions/ERC721Mint/ERC721Mint.sol index 82a7651..dbf1db1 100644 --- a/src/hooks/actions/ERC721Mint/ERC721Mint.sol +++ b/src/hooks/actions/ERC721Mint/ERC721Mint.sol @@ -23,7 +23,8 @@ 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; /*////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -43,12 +44,12 @@ contract ERC721Mint is RegistryProductAction { function _onProductPurchase( uint256 slicerId, uint256 productId, - uint256, + uint256 variantId, address buyer, uint256 quantity, 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)); @@ -62,7 +63,10 @@ contract ERC721Mint is RegistryProductAction { * @inheritdoc HookRegistry * @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 + { ( string memory name_, string memory symbol_, @@ -76,7 +80,7 @@ contract ERC721Mint is RegistryProductAction { if (royaltyFraction_ > MAX_ROYALTY) revert InvalidRoyaltyFraction(); - ERC721Mint_BaseToken token = tokenData[slicerId][productId].token; + ERC721Mint_BaseToken token = tokenData[slicerId][productId][variantId].token; if (address(token) == address(0)) { token = new ERC721Mint_BaseToken( @@ -86,13 +90,13 @@ contract ERC721Mint is RegistryProductAction { token.setParams(maxSupply, royaltyReceiver_, royaltyFraction_, baseURI__, tokenURI__); } - tokenData[slicerId][productId] = ERC721Data(token, revertOnMaxSupplyReached); + tokenData[slicerId][productId][variantId] = ERC721Data(token, revertOnMaxSupplyReached); } /** * @inheritdoc IHookRegistry */ - function paramsSchema() external pure override returns (string memory) { + function configureProductSchema() external pure override returns (string memory) { return "string name,string symbol,address royaltyReceiver,uint256 royaltyFraction,string baseURI,string tokenURI,bool revertOnMaxSupplyReached,uint256 maxSupply"; } diff --git a/src/hooks/actions/NFTGated/NFTGated.sol b/src/hooks/actions/NFTGated/NFTGated.sol index 5918ea3..c7d020b 100644 --- a/src/hooks/actions/NFTGated/NFTGated.sol +++ b/src/hooks/actions/NFTGated/NFTGated.sol @@ -22,7 +22,8 @@ contract NFTGated is RegistryProductAction { 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; /*////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -38,13 +39,15 @@ contract NFTGated is RegistryProductAction { * @inheritdoc IProductAction * @dev Checks if `account` owns the required amount of NFT tokens. */ - function isPurchaseAllowed(uint256 slicerId, uint256 productId, uint256, address buyer, uint256, bytes memory) - public - view - override - returns (bool isAllowed) - { - NFTGates memory nftGates_ = nftGates[slicerId][productId]; + function isPurchaseAllowed( + uint256 slicerId, + uint256 productId, + uint256 variantId, + address buyer, + uint256, + bytes memory + ) public view override returns (bool isAllowed) { + NFTGates memory nftGates_ = nftGates[slicerId][productId][variantId]; uint256 totalOwned; unchecked { @@ -70,21 +73,24 @@ contract NFTGated is RegistryProductAction { * @inheritdoc HookRegistry * @dev Set the NFT gates 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 + { (NFTGates memory nftGates_) = abi.decode(params, (NFTGates)); - delete nftGates[slicerId][productId].gates; + delete nftGates[slicerId][productId][variantId].gates; - nftGates[slicerId][productId].minOwned = nftGates_.minOwned; + nftGates[slicerId][productId][variantId].minOwned = nftGates_.minOwned; for (uint256 i = 0; i < nftGates_.gates.length; i++) { - nftGates[slicerId][productId].gates.push(nftGates_.gates[i]); + nftGates[slicerId][productId][variantId].gates.push(nftGates_.gates[i]); } } /** * @inheritdoc IHookRegistry */ - function paramsSchema() external pure override returns (string memory) { + function configureProductSchema() external pure override returns (string memory) { return "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned"; } } 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..a820cdc 100644 --- a/src/hooks/pricing/README.md +++ b/src/hooks/pricing/README.md @@ -31,7 +31,7 @@ The script will create your contract file with the proper template and add it to Strategies inheriting from `RegistryProductPrice` automatically support frontend integration through: - **Product configuration** via `configureProduct()` -- **Parameter validation** via `paramsSchema()` +- **Parameter validation** via `configureProductSchema()` ### Testing diff --git a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol index 200c512..7d81097 100644 --- a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol @@ -28,12 +28,15 @@ contract NFTDiscount is TieredDiscount { * @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 { + function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) + internal + override + { (DiscountParams[] memory newDiscounts) = abi.decode(params, (DiscountParams[])); - DiscountParams[] storage productDiscount = discounts[slicerId][productId]; + DiscountParams[] storage productDiscount = discounts[slicerId][productId][variantId]; - delete discounts[slicerId][productId]; + delete discounts[slicerId][productId][variantId]; uint256 prevDiscountValue; DiscountParams memory discountParam; @@ -68,7 +71,7 @@ contract NFTDiscount is TieredDiscount { /** * @inheritdoc IHookRegistry */ - function paramsSchema() external pure override returns (string memory) { + function configureProductSchema() external pure override returns (string memory) { return "(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[] discounts"; } @@ -81,7 +84,7 @@ contract NFTDiscount is TieredDiscount { uint256, uint256, address, - uint256 quantity, + uint256, address buyer, bytes memory, uint256 basePriceAmount, @@ -89,7 +92,7 @@ contract NFTDiscount is TieredDiscount { ) internal view virtual override returns (uint256 price) { uint256 discount = _getHighestDiscount(discountParams, buyer); - price = discount != 0 ? _getDiscountedPrice(basePriceAmount, discount, quantity) : quantity * basePriceAmount; + price = discount != 0 ? _getDiscountedPrice(basePriceAmount, discount) : basePriceAmount; } /*////////////////////////////////////////////////////////////// @@ -152,11 +155,10 @@ contract NFTDiscount is TieredDiscount { * * @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 basePriceAmount, uint256 discount, uint256 quantity) + function _getDiscountedPrice(uint256 basePriceAmount, uint256 discount) internal pure virtual @@ -168,6 +170,6 @@ contract NFTDiscount is TieredDiscount { k = 1e4 - discount; } - price = (basePriceAmount * k * quantity) / 1e4; + price = (basePriceAmount * k) / 1e4; } } diff --git a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol index 767c240..984886b 100644 --- a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol @@ -25,7 +25,8 @@ 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; /*////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -50,7 +51,7 @@ abstract contract TieredDiscount is RegistryProductPrice { address buyer, bytes memory data ) public view override returns (uint256) { - DiscountParams[] memory discountParams = discounts[slicerId][productId]; + DiscountParams[] memory discountParams = discounts[slicerId][productId][variantId]; return _productPrice(slicerId, productId, variantId, currency, quantity, buyer, data, basePrice, discountParams); } diff --git a/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol index 3f2a057..2f6bf69 100644 --- a/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol @@ -16,7 +16,8 @@ contract LinearVRGDAPrices is VRGDAPrices { //////////////////////////////////////////////////////////////*/ // Mapping from slicerId to productId to ProductParams - mapping(uint256 => mapping(uint256 => LinearProduct)) private _productParams; + mapping(uint256 slicerId => mapping(uint256 productId => mapping(uint256 variantId => LinearProduct))) private + _productParams; /*////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -30,9 +31,12 @@ contract LinearVRGDAPrices is VRGDAPrices { /** * @inheritdoc HookRegistry - * @notice Set LinearVRGDAParams for a product. + * @notice Set LinearProduct 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 + { (int184 priceDecayPercent, uint16 min, int256 perTimeUnit) = abi.decode(params, (int184, uint16, int256)); int256 decayConstant = wadLn(1e18 - priceDecayPercent); @@ -41,13 +45,13 @@ contract LinearVRGDAPrices is VRGDAPrices { require(decayConstant >= type(int184).min, "MIN_DECAY_CONSTANT_EXCEEDED"); /// Get product availability and isInfinite - (uint256 stockUnits_, bool isInfinite) = stockUnits(slicerId, productId, 0); + (uint256 stockUnits_, bool isInfinite) = getStockUnits(slicerId, productId, 0); // Product must not have infinite availability require(!isInfinite, "NOT_FINITE_AVAILABILITY"); // Set product params - _productParams[slicerId][productId] = LinearProduct({ + _productParams[slicerId][productId][variantId] = LinearProduct({ startTime: uint40(block.timestamp), startUnits: uint32(stockUnits_), decayConstant: int184(decayConstant), @@ -69,13 +73,13 @@ contract LinearVRGDAPrices is VRGDAPrices { address, bytes memory ) public view override returns (uint256 price) { - LinearProduct storage productParams = _productParams[slicerId][productId]; + LinearProduct storage productParams = _productParams[slicerId][productId][variantId]; require(productParams.startTime != 0, "PRODUCT_UNSET"); // Get available units and base price - (uint256 stockUnits_,) = stockUnits(slicerId, productId, variantId); - uint256 basePrice_ = basePrice(slicerId, productId, variantId, currency, 1); + (uint256 stockUnits_,) = getStockUnits(slicerId, productId, variantId); + uint256 basePrice_ = getBasePrice(slicerId, productId, variantId, currency, 1); // Calculate sold units from stockUnits uint256 soldUnits = productParams.startUnits - stockUnits_; @@ -94,7 +98,7 @@ contract LinearVRGDAPrices is VRGDAPrices { /** * @inheritdoc IHookRegistry */ - function paramsSchema() external pure override returns (string memory) { + function configureProductSchema() external 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 111d591..ce57c43 100644 --- a/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol @@ -25,7 +25,8 @@ contract LogisticVRGDAPrices is VRGDAPrices { //////////////////////////////////////////////////////////////*/ // Mapping from slicerId to productId to LogisticProduct - mapping(uint256 => mapping(uint256 => LogisticProduct)) private _productParams; + mapping(uint256 slicerId => mapping(uint256 productId => mapping(uint256 variantId => LogisticProduct))) private + _productParams; /*////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -41,7 +42,10 @@ contract LogisticVRGDAPrices is VRGDAPrices { * @inheritdoc HookRegistry * @notice Set LogisticVRGDAParams 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 + { (int184 priceDecayPercent, uint16 min, int256 timescale) = abi.decode(params, (int184, uint16, int256)); int256 decayConstant = wadLn(1e18 - priceDecayPercent); @@ -50,13 +54,13 @@ contract LogisticVRGDAPrices is VRGDAPrices { require(decayConstant >= type(int184).min, "MIN_DECAY_CONSTANT_EXCEEDED"); // Get product availability and isInfinite - (uint256 stockUnits_, bool isInfinite) = stockUnits(slicerId, productId, 0); + (uint256 stockUnits_, bool isInfinite) = getStockUnits(slicerId, productId, 0); // Product must not have infinite availability require(!isInfinite, "NON_FINITE_AVAILABILITY"); // Set product params - _productParams[slicerId][productId] = LogisticProduct({ + _productParams[slicerId][productId][variantId] = LogisticProduct({ startTime: uint40(block.timestamp), startUnits: uint32(stockUnits_), decayConstant: int184(decayConstant), @@ -78,13 +82,13 @@ contract LogisticVRGDAPrices is VRGDAPrices { address, bytes memory ) public view override returns (uint256 price) { - LogisticProduct storage productParams = _productParams[slicerId][productId]; + LogisticProduct storage productParams = _productParams[slicerId][productId][variantId]; require(productParams.startTime != 0, "PRODUCT_UNSET"); // Get available units and base price - (uint256 stockUnits_,) = stockUnits(slicerId, productId, variantId); - uint256 basePrice_ = basePrice(slicerId, productId, variantId, currency, 1); + (uint256 stockUnits_,) = getStockUnits(slicerId, productId, variantId); + uint256 basePrice_ = getBasePrice(slicerId, productId, variantId, currency, 1); // Calculate sold units from stockUnits uint256 soldUnits = productParams.startUnits - stockUnits_; @@ -104,7 +108,7 @@ contract LogisticVRGDAPrices is VRGDAPrices { /** * @inheritdoc IHookRegistry */ - function paramsSchema() external pure override returns (string memory) { + function configureProductSchema() external pure override returns (string memory) { return "int184 priceDecayPercent,uint16 min,int256 timescale"; } diff --git a/src/hooks/pricingActions/FirstForFree/FirstForFree.sol b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol index e8b0974..bf63c03 100644 --- a/src/hooks/pricingActions/FirstForFree/FirstForFree.sol +++ b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol @@ -26,8 +26,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 @@ -46,50 +46,48 @@ contract FirstForFree is RegistryProductPriceAction { function productPrice( uint256 slicerId, uint256 productId, - uint256, + uint256 variantId, address, - uint256, + uint256 basePrice, uint256 quantity, address buyer, bytes memory ) public view override returns (uint256) { - ProductParams memory productParams = usdcPrices[slicerId][productId]; + 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; } else { - return usdcPrices[slicerId][productId].usdcPrice * (quantity - freeUnitsLeft); + return getBasePrice(slicerId, productId, variantId, address(0), quantity - freeUnitsLeft); } } } } - return 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, + uint256 variantId, address buyer, uint256 quantity, 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); } } @@ -97,27 +95,24 @@ contract FirstForFree is RegistryProductPriceAction { * @inheritdoc HookRegistry * @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; @@ -128,9 +123,9 @@ contract FirstForFree is RegistryProductPriceAction { /** * @inheritdoc IHookRegistry */ - function paramsSchema() external pure override returns (string memory) { + function configureProductSchema() external 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,uint88 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/IProductPriceV1.sol b/src/interfaces/IProductAction.sol similarity index 52% rename from src/interfaces/IProductPriceV1.sol rename to src/interfaces/IProductAction.sol index 81a8767..fd6fbf9 100644 --- a/src/interfaces/IProductPriceV1.sol +++ b/src/interfaces/IProductAction.sol @@ -1,4 +1,4 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import "slice/interfaces/hooks/IProductPriceV1.sol"; +import {IProductAction} from "slice/interfaces.sol"; diff --git a/src/interfaces/IProductActionV1.sol b/src/interfaces/IProductPrice.sol similarity index 51% rename from src/interfaces/IProductActionV1.sol rename to src/interfaces/IProductPrice.sol index 796f503..0b4afc9 100644 --- a/src/interfaces/IProductActionV1.sol +++ b/src/interfaces/IProductPrice.sol @@ -1,4 +1,4 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import "slice/interfaces/hooks/IProductActionV1.sol"; +import {IProductPrice} from "slice/interfaces.sol"; diff --git a/test/actions/Allowlisted/Allowlisted.t.sol b/test/actions/Allowlisted/Allowlisted.t.sol index 3c3237d..e0d4daf 100644 --- a/test/actions/Allowlisted/Allowlisted.t.sol +++ b/test/actions/Allowlisted/Allowlisted.t.sol @@ -7,6 +7,7 @@ import {Allowlisted} from "@/hooks/actions/Allowlisted/Allowlisted.sol"; uint256 constant slicerId = 0; uint256 constant productId = 1; +uint256 constant variantId = 0; contract AllowlistedTest is RegistryProductActionTest { Allowlisted allowlisted; @@ -29,28 +30,28 @@ 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, 0, 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, 0, buyer, 0, abi.encode(wrongProof))); + assertFalse(allowlisted.isPurchaseAllowed(slicerId, productId, variantId, buyer, 0, abi.encode(wrongProof))); } } diff --git a/test/actions/ERC20Gated/ERC20Gated.t.sol b/test/actions/ERC20Gated/ERC20Gated.t.sol index 7fcfed7..424c190 100644 --- a/test/actions/ERC20Gated/ERC20Gated.t.sol +++ b/test/actions/ERC20Gated/ERC20Gated.t.sol @@ -8,6 +8,7 @@ import {IERC20, MockERC20} from "@test/utils/mocks/MockERC20.sol"; uint256 constant slicerId = 0; uint256 constant productId = 1; +uint256 constant variantId = 0; contract ERC20GatedTest is RegistryProductActionTest { MockERC20Gated erc20Gated; @@ -24,9 +25,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 +38,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 +63,11 @@ 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, 0, buyer, 0, "")); + assertFalse(erc20Gated.isPurchaseAllowed(slicerId, productId, variantId, buyer, 0, "")); token.mint(buyer, 100); - assertTrue(erc20Gated.isPurchaseAllowed(slicerId, productId, 0, buyer, 0, "")); + assertTrue(erc20Gated.isPurchaseAllowed(slicerId, productId, variantId, buyer, 0, "")); } } diff --git a/test/actions/ERC20Gated/mocks/MockERC20Gated.sol b/test/actions/ERC20Gated/mocks/MockERC20Gated.sol index ea96650..d5202a5 100644 --- a/test/actions/ERC20Gated/mocks/MockERC20Gated.sol +++ b/test/actions/ERC20Gated/mocks/MockERC20Gated.sol @@ -6,7 +6,7 @@ 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]; } } diff --git a/test/actions/ERC20Mint/ERC20Mint.t.sol b/test/actions/ERC20Mint/ERC20Mint.t.sol index 60d484d..9478e96 100644 --- a/test/actions/ERC20Mint/ERC20Mint.t.sol +++ b/test/actions/ERC20Mint/ERC20Mint.t.sol @@ -9,6 +9,7 @@ import {ERC20Mint_BaseToken} from "@/hooks/actions/ERC20Mint/utils/ERC20Mint_Bas import {console2} from "forge-std/console2.sol"; uint256 constant slicerId = 0; +uint256 constant variantId = 0; contract ERC20MintTest is RegistryProductActionTest { ERC20Mint erc20Mint; @@ -27,6 +28,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test Token 1", // name "TT1", // symbol @@ -42,6 +44,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[1], + variantId, abi.encode( "Test Token 2", // name "TT2", // symbol @@ -57,6 +60,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[2], + variantId, abi.encode( "Test Token 3", // name "TT3", // symbol @@ -72,7 +76,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 +86,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 +95,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 +109,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test Token", // name "TT", // symbol @@ -116,13 +121,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 +141,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 +163,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test Token", // name "TT", // symbol @@ -178,6 +185,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test Token", // name "TT", // symbol @@ -193,6 +201,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[1], + variantId, abi.encode( "Test Token 2", // name "TT2", // symbol @@ -228,6 +237,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test Token 1", // name "TT1", // symbol @@ -242,6 +252,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[1], + variantId, abi.encode( "Test Token 2", // name "TT2", // symbol @@ -255,15 +266,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], 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 +284,14 @@ contract ERC20MintTest is RegistryProductActionTest { uint256 initialSupply2 = token2.totalSupply(); vm.prank(address(PRODUCTS_MODULE)); - erc20Mint.onProductPurchase(slicerId, productIds[1], 0, 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], 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 +303,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test Token", // name "TT", // symbol @@ -306,12 +318,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], 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 +337,7 @@ contract ERC20MintTest is RegistryProductActionTest { erc20Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test Token", // name "TT", // symbol @@ -342,12 +355,12 @@ contract ERC20MintTest is RegistryProductActionTest { vm.prank(address(PRODUCTS_MODULE)); 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], 0, buyer, 1, ""); + erc20Mint.onProductPurchase(slicerId, productIds[0], variantId, buyer, 1, ""); } } diff --git a/test/actions/ERC721Mint/ERC721Mint.t.sol b/test/actions/ERC721Mint/ERC721Mint.t.sol index 1c7d6f3..f4b3729 100644 --- a/test/actions/ERC721Mint/ERC721Mint.t.sol +++ b/test/actions/ERC721Mint/ERC721Mint.t.sol @@ -9,6 +9,7 @@ import {ERC721Mint_BaseToken, MAX_ROYALTY} from "@/hooks/actions/ERC721Mint/util import {console2} from "forge-std/console2.sol"; uint256 constant slicerId = 0; +uint256 constant variantId = 0; contract ERC721MintTest is RegistryProductActionTest { ERC721Mint erc721Mint; @@ -27,6 +28,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test NFT 1", // name "TNT1", // symbol @@ -43,6 +45,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[1], + variantId, abi.encode( "Test NFT 2", // name "TNT2", // symbol @@ -59,6 +62,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[2], + variantId, abi.encode( "Test NFT 3", // name "TNT3", // symbol @@ -74,7 +78,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 +90,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 +102,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 +119,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test NFT", // name "TNT", // symbol @@ -124,13 +132,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 +152,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 +177,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test NFT", // name "TNT", // symbol @@ -187,6 +198,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test NFT 1", // name "TNT1", // symbol @@ -202,6 +214,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[1], + variantId, abi.encode( "Test NFT 2", // name "TNT2", // symbol @@ -216,15 +229,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], 0, buyer, 3, ""); + erc721Mint.onProductPurchase(slicerId, productIds[0], variantId, buyer, 3, ""); assertEq(token1.balanceOf(buyer), initialBalance1 + 3); assertEq(token1.totalSupply(), initialSupply1 + 3); @@ -234,14 +247,14 @@ contract ERC721MintTest is RegistryProductActionTest { uint256 initialSupply2 = token2.totalSupply(); vm.prank(address(PRODUCTS_MODULE)); - erc721Mint.onProductPurchase(slicerId, productIds[1], 0, 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], 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 +266,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test NFT", // name "TNT", // symbol @@ -265,16 +279,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], 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], 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 +296,7 @@ contract ERC721MintTest is RegistryProductActionTest { uint256 supplyBefore = token.totalSupply(); vm.prank(address(PRODUCTS_MODULE)); - erc721Mint.onProductPurchase(slicerId, productIds[0], 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 +310,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test NFT", // name "TNT", // symbol @@ -312,18 +327,18 @@ contract ERC721MintTest is RegistryProductActionTest { vm.prank(address(PRODUCTS_MODULE)); 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], 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], 0, buyer3, 1, ""); + erc721Mint.onProductPurchase(slicerId, productIds[0], variantId, buyer3, 1, ""); } function testTokenURI() public { @@ -333,6 +348,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test NFT", // name "TNT", // symbol @@ -349,6 +365,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[1], + variantId, abi.encode( "Test NFT 2", // name "TNT2", // symbol @@ -363,15 +380,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], 0, buyer, 2, ""); + erc721Mint.onProductPurchase(slicerId, productIds[0], variantId, buyer, 2, ""); vm.prank(address(PRODUCTS_MODULE)); - erc721Mint.onProductPurchase(slicerId, productIds[1], 0, 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"); @@ -386,6 +403,7 @@ contract ERC721MintTest is RegistryProductActionTest { erc721Mint.configureProduct( slicerId, productIds[0], + variantId, abi.encode( "Test NFT", // name "TNT", // symbol @@ -398,7 +416,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); diff --git a/test/actions/NFTGated/NFTGated.t.sol b/test/actions/NFTGated/NFTGated.t.sol index ee633bf..a0af4f7 100644 --- a/test/actions/NFTGated/NFTGated.t.sol +++ b/test/actions/NFTGated/NFTGated.t.sol @@ -17,6 +17,7 @@ contract NFTGatedTest is RegistryProductActionTest { MockERC1155 nft1155 = new MockERC1155(); uint256[] productIds = [1, 2, 3, 4]; + uint256 variantId = 0; function setUp() public { nftGated = new MockNFTGated(PRODUCTS_MODULE); @@ -28,8 +29,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, abi.encode(nftGates[i])); + assertEq(nftGated.nftGates(slicerId, productIds[i], variantId), nftGates[i].minOwned); } vm.stopPrank(); } @@ -39,14 +40,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, abi.encode(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, abi.encode(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 +57,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, abi.encode(nftGates[i])); } vm.stopPrank(); @@ -67,22 +68,22 @@ contract NFTGatedTest is RegistryProductActionTest { nft1155.mint(buyer3); // buyer should be able to purchase all products - assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[0], 0, buyer, 0, "")); - assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[1], 0, buyer, 0, "")); - assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[2], 0, buyer, 0, "")); - assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[3], 0, 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], 0, buyer2, 0, "")); - assertFalse(nftGated.isPurchaseAllowed(slicerId, productIds[1], 0, buyer2, 0, "")); - assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[2], 0, buyer2, 0, "")); - assertFalse(nftGated.isPurchaseAllowed(slicerId, productIds[3], 0, 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], 0, buyer3, 0, "")); - assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[1], 0, buyer3, 0, "")); - assertTrue(nftGated.isPurchaseAllowed(slicerId, productIds[2], 0, buyer3, 0, "")); - assertFalse(nftGated.isPurchaseAllowed(slicerId, productIds[3], 0, 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, "")); } /*////////////////////////////////////////////////////////////// diff --git a/test/actions/NFTGated/mocks/MockNFTGated.sol b/test/actions/NFTGated/mocks/MockNFTGated.sol index d246076..619d849 100644 --- a/test/actions/NFTGated/mocks/MockNFTGated.sol +++ b/test/actions/NFTGated/mocks/MockNFTGated.sol @@ -6,7 +6,7 @@ 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; } } diff --git a/test/pricing/TieredDiscount/NFTDiscount.t.sol b/test/pricing/TieredDiscount/NFTDiscount.t.sol index ef2976b..fb655cb 100644 --- a/test/pricing/TieredDiscount/NFTDiscount.t.sol +++ b/test/pricing/TieredDiscount/NFTDiscount.t.sol @@ -16,6 +16,7 @@ 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; uint80 constant percentDiscountOne = 1000; // 10% uint80 constant percentDiscountTwo = 2000; // 20% @@ -26,7 +27,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 { @@ -49,15 +50,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 - Price memory price = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, 0, ETH, quantity); + /// check product price + uint256 price = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(price.eth, quantity * (basePrice.eth - (basePrice.eth * percentDiscountOne) / 1e4)); - assertEq(price.currency, 0); + assertEq(price, (basePrice - (basePrice * percentDiscountOne) / 1e4)); } function testConfigureProduct__ERC20() public { @@ -73,15 +74,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 - Price memory price = erc721GatedDiscount.productPrice(slicerId, productId, 0, USDC, quantity, buyer, ""); + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, USDC, quantity); - Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, 0, USDC, quantity); + /// check product price + uint256 price = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, USDC, basePrice, quantity, buyer, ""); - assertEq(price.currency, quantity * (basePrice.currency - (basePrice.currency * percentDiscountOne) / 1e4)); - assertTrue(price.eth == 0); + assertEq(price, (basePrice - (basePrice * percentDiscountOne) / 1e4)); } function testConfigureProduct__ERC1155() public { @@ -97,22 +98,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 - Price memory price = erc721GatedDiscount.productPrice(slicerId, productId, 0, USDC, quantity, buyer, ""); + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, USDC, quantity); - Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, 0, USDC, quantity); + /// check product price + uint256 price = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, USDC, basePrice, quantity, buyer, ""); - assertEq(price.currency, quantity * basePrice.currency); - assertEq(price.eth, 0); + assertEq(price, basePrice); nft1155.mint(buyer); - price = erc721GatedDiscount.productPrice(slicerId, productId, 0, USDC, quantity, buyer, ""); + price = erc721GatedDiscount.productPrice(slicerId, productId, variantId, USDC, basePrice, quantity, buyer, ""); - assertEq(price.currency, quantity * (basePrice.currency - (basePrice.currency * percentDiscountOne) / 1e4)); - assertEq(price.eth, 0); + assertEq(price, (basePrice - (basePrice * percentDiscountOne) / 1e4)); } function testConfigureProduct__HigherDiscount() public { @@ -134,22 +134,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 - Price memory price = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, 0, ETH, quantity); + /// check product price for ETH + uint256 price = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(price.eth, quantity * (basePrice.eth - (basePrice.eth * percentDiscountOne) / 1e4)); - assertEq(price.currency, 0); + assertEq(price, (basePrice - (basePrice * percentDiscountOne) / 1e4)); nft1155.mint(buyer); - price = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); + price = erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(price.eth, quantity * (basePrice.eth - (basePrice.eth * percentDiscountTwo) / 1e4)); - assertEq(price.currency, 0); + assertEq(price, (basePrice - (basePrice * percentDiscountTwo) / 1e4)); } function testRevert_ProductPrice__NotNFTOwner() public { @@ -165,15 +164,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 - Price memory price = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, 0, ETH, quantity); + /// check product price + uint256 price = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(price.eth, quantity * basePrice.eth); - assertEq(price.currency, 0); + assertEq(price, basePrice); } function testProductPrice__MinQuantity() public { @@ -189,24 +188,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 - Price memory price = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, 0, ETH, quantity); + /// check product price + uint256 price = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(price.eth, quantity * basePrice.eth); - assertEq(price.currency, 0); + assertEq(price, basePrice); /// Buyer owns 2 NFTs, minQuantity is 2 nftOne.mint(buyer); /// check product price - Price memory secondPrice = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); + uint256 secondPrice = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(secondPrice.eth, quantity * (basePrice.eth - (basePrice.eth * percentDiscountOne) / 1e4)); - assertEq(secondPrice.currency, 0); + assertEq(secondPrice, (basePrice - (basePrice * percentDiscountOne) / 1e4)); } function testProductPrice__MultipleBoughtQuantity() public { @@ -221,18 +220,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 - Price memory price = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, 0, ETH, quantity); + /// check product price + uint256 price = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(price.eth, quantity * (basePrice.eth - (basePrice.eth * percentDiscountOne) / 1e4)); - assertEq(price.currency, 0); + assertEq(price, (basePrice - (basePrice * percentDiscountOne) / 1e4)); } function testConfigureProduct__Edit_Add() public { @@ -247,18 +246,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 - Price memory price = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, 0, ETH, quantity); + /// check product price + uint256 price = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(price.eth, quantity * (basePrice.eth - (basePrice.eth * percentDiscountTwo) / 1e4)); - assertEq(price.currency, 0); + assertEq(price, (basePrice - (basePrice * percentDiscountTwo) / 1e4)); discountParams = new DiscountParams[](2); @@ -280,13 +279,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 - Price memory secondPrice = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); + uint256 secondPrice = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(secondPrice.eth, quantity * (basePrice.eth - (basePrice.eth * percentDiscountOne) / 1e4)); - assertEq(secondPrice.currency, 0); + assertEq(secondPrice, (basePrice - (basePrice * percentDiscountOne) / 1e4)); } function testConfigureProduct__Edit_Remove() public { @@ -313,25 +312,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 - Price memory secondPrice = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); + uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - Price memory basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, 0, ETH, quantity); + /// check product price + uint256 secondPrice = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(secondPrice.eth, quantity * (basePrice.eth - (basePrice.eth * percentDiscountOne) / 1e4)); - assertEq(secondPrice.currency, 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 - Price memory price = erc721GatedDiscount.productPrice(slicerId, productId, 0, ETH, quantity, buyer, ""); + uint256 price = + erc721GatedDiscount.productPrice(slicerId, productId, variantId, ETH, basePrice, quantity, buyer, ""); - assertEq(price.eth, quantity * basePrice.eth); - assertEq(price.currency, 0); + assertEq(price, basePrice); } } diff --git a/test/pricing/VRGDA/LinearVRGDA.t.sol b/test/pricing/VRGDA/LinearVRGDA.t.sol index a651816..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,15 +136,13 @@ contract LinearVRGDATest is RegistryProductPriceTest { vm.warp(block.timestamp + timeDelta); - Price memory price = vrgda.productPrice(slicerId, productId_, 0, address(0), 1, address(0), ""); + uint256 price = vrgda.productPrice(slicerId, productId_, variantId, address(0), 0, 1, address(0), ""); - assertApproxEqAbs(uint256(uint128(targetPriceConstant)), price.eth, 0.00001e18); - assertEq(price.currency, 0); + assertApproxEqAbs(uint256(uint128(targetPriceConstant)), price, 0.00001e18); - Price memory price2 = vrgda.productPrice(slicerId, productId_, 0, address(20), 1, address(0), ""); + price = vrgda.productPrice(slicerId, productId_, variantId, address(20), 0, 1, address(0), ""); - assertEq(price2.eth, 0); - assertApproxEqAbs(uint256(uint128(targetPriceConstant)), price2.currency, 0.00001e18); + assertApproxEqAbs(uint256(uint128(targetPriceConstant_Erc20)), price, 0.00001e18); } function testProductPriceEth() public { @@ -158,13 +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 ); - Price memory price = vrgda.productPrice(slicerId, productId, 0, ethCurrency, 1, address(0), ""); + uint256 price = vrgda.productPrice(slicerId, productId, variantId, ethCurrency, 0, 1, address(0), ""); - assertApproxEqAbs(cost, price.eth, 0.00001e18); - assertEq(price.currency, 0); + assertApproxEqAbs(cost, price, 0.00001e18); } function testProductPriceErc20() public { @@ -178,13 +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 ); - Price memory price = vrgda.productPrice(slicerId, productId, 0, erc20Currency, 1, address(0), ""); + uint256 price = vrgda.productPrice(slicerId, productId, variantId, erc20Currency, 0, 1, address(0), ""); - assertApproxEqAbs(cost, price.currency, 0.00001e18); - assertEq(price.eth, 0); + assertApproxEqAbs(cost, price, 0.00001e18); } function testProductPriceMultiple() public { @@ -198,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 ); - Price memory price = vrgda.productPrice(slicerId, productId, 0, ethCurrency, 3, address(0), ""); + uint256 price = vrgda.productPrice(slicerId, productId, variantId, ethCurrency, 0, 3, address(0), ""); - assertApproxEqAbs(costMultiple, price.eth, 5e14); + assertApproxEqAbs(costMultiple, price, 5e14); } } diff --git a/test/pricing/VRGDA/LogisticVRGDA.t.sol b/test/pricing/VRGDA/LogisticVRGDA.t.sol index f8af08d..87a9e87 100644 --- a/test/pricing/VRGDA/LogisticVRGDA.t.sol +++ b/test/pricing/VRGDA/LogisticVRGDA.t.sol @@ -13,28 +13,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 +58,7 @@ contract LogisticVRGDATest is RegistryProductPriceTest { logisticLimit * 2e18, sold, timeScale, - min + minAmount ); assertApproxEqAbs(cost, uint256(uint128(targetPriceConstant)), 1e13); } @@ -77,7 +80,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 +104,9 @@ contract LogisticVRGDATest is RegistryProductPriceTest { logisticLimit * 2e18, numMint, timeScale, - min + minAmount ); - assertEq(cost, min); + assertEq(cost, minAmount); } function testPricingAdjustedByQuantity() public { @@ -121,7 +124,7 @@ contract LogisticVRGDATest is RegistryProductPriceTest { toWadUnsafe(MAX_SELLABLE + 1), numMint, timeScale, - min, + minAmount, 1 ); uint256 costProduct2 = vrgda.getAdjustedVRGDALogisticPrice( @@ -131,7 +134,7 @@ contract LogisticVRGDATest is RegistryProductPriceTest { toWadUnsafe(MAX_SELLABLE + 1), numMint + 1, timeScale, - min, + minAmount, 1 ); uint256 costProduct3 = vrgda.getAdjustedVRGDALogisticPrice( @@ -141,7 +144,7 @@ contract LogisticVRGDATest is RegistryProductPriceTest { toWadUnsafe(MAX_SELLABLE + 1), numMint + 2, timeScale, - min, + minAmount, 1 ); uint256 costMultiple = vrgda.getAdjustedVRGDALogisticPrice( @@ -151,7 +154,7 @@ contract LogisticVRGDATest is RegistryProductPriceTest { toWadUnsafe(MAX_SELLABLE + 1), numMint, timeScale, - min, + minAmount, 3 ); @@ -161,28 +164,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); - Price memory price = vrgda.productPrice(slicerId, productIdTest, 0, address(0), 1, address(0), ""); + uint256 price = vrgda.productPrice(slicerId, productIdTest, variantId, address(0), 0, 1, address(0), ""); // assertApproxEqAbs(uint256(targetPriceTest), ethPrice, 1e18); - assertNotEq(price.eth, 0); - assertEq(price.currency, 0); + assertNotEq(price, 0); - Price memory price2 = vrgda.productPrice(slicerId, productId, 0, address(20), 1, address(0), ""); + uint256 price2 = vrgda.productPrice(slicerId, productId, variantId, address(20), 0, 1, address(0), ""); - assertEq(price2.eth, 0); - assertNotEq(price2.currency, 0); + assertNotEq(price2, 0); } function testProductPriceEth() public { @@ -201,14 +199,13 @@ contract LogisticVRGDATest is RegistryProductPriceTest { toWadUnsafe(MAX_SELLABLE + 1), 0, timeScale, - min, + minAmount, 1 ); - Price memory price = vrgda.productPrice(slicerId, productId, 0, ethCurrency, 1, address(0), ""); + uint256 price = vrgda.productPrice(slicerId, productId, variantId, ethCurrency, 0, 1, address(0), ""); - assertApproxEqAbs(cost, price.eth, 0.00001e18); - assertEq(price.currency, 0); + assertApproxEqAbs(cost, price, 0.00001e18); } function testProductPriceErc20() public { @@ -221,20 +218,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 ); - Price memory price = vrgda.productPrice(slicerId, productId, 0, erc20Currency, 1, address(0), ""); + uint256 price = vrgda.productPrice(slicerId, productId, variantId, erc20Currency, 0, 1, address(0), ""); - assertApproxEqAbs(cost, price.currency, 0.00001e18); - assertEq(price.eth, 0); + assertApproxEqAbs(cost, price, 0.00001e18); } function testProductPriceMultiple() public { @@ -253,12 +249,12 @@ contract LogisticVRGDATest is RegistryProductPriceTest { toWadUnsafe(MAX_SELLABLE + 1), 0, timeScale, - min, + minAmount, 3 ); - Price memory price = vrgda.productPrice(slicerId, productId, 0, ethCurrency, 3, address(0), ""); + uint256 price = vrgda.productPrice(slicerId, productId, variantId, ethCurrency, 0, 3, address(0), ""); - assertApproxEqAbs(costMultiple, price.eth, 5e14); + assertApproxEqAbs(costMultiple, price, 5e14); } function testGetTargetSaleTimeDoesNotRevertEarly() public view { @@ -288,7 +284,7 @@ contract LogisticVRGDATest is RegistryProductPriceTest { logisticLimit * 2e18, bound(sold, 0, 1730), timeScale, - min + minAmount ); } @@ -304,7 +300,7 @@ contract LogisticVRGDATest is RegistryProductPriceTest { logisticLimit * 2e18, bound(sold, 0, 6391), timeScale, - min + minAmount ); } @@ -321,7 +317,7 @@ contract LogisticVRGDATest is RegistryProductPriceTest { logisticLimit * 2e18, bound(sold, 6392, type(uint128).max), timeScale, - min + minAmount ); } @@ -340,7 +336,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..bc87461 100644 --- a/test/pricing/VRGDA/correctness/LinearVRGDACorrectness.t.sol +++ b/test/pricing/VRGDA/correctness/LinearVRGDACorrectness.t.sol @@ -6,7 +6,7 @@ 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 +15,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 +27,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 { diff --git a/test/pricingActions/FirstForFree/FirstForFree.t.sol b/test/pricingActions/FirstForFree/FirstForFree.t.sol index a817d4f..9e241b6 100644 --- a/test/pricingActions/FirstForFree/FirstForFree.t.sol +++ b/test/pricingActions/FirstForFree/FirstForFree.t.sol @@ -10,8 +10,6 @@ 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 +30,13 @@ 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 variantId = 0; + + function _basePrice(uint256 slicerId_, uint256 productId_, 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 +55,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 +76,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 +97,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 +120,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,33 +132,29 @@ 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); @@ -169,8 +168,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,40 +178,40 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { ); // First purchase - should be free - Price memory price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, buyer, ""); - assertEq(price.eth, 0); - assertEq(price.currency, 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 - price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, buyer, ""); - assertEq(price.eth, 0); - assertEq(price.currency, 0); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, buyer, ""); + assertEq(price, 0); // Partial free purchase - price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 2, buyer, ""); - assertEq(price.eth, 0); - assertEq(price.currency, 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 - price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 3, buyer, ""); - assertEq(price.eth, 0); - assertEq(price.currency, 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], 0, buyer, 1, ""); - - price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 2, buyer, ""); - assertEq(price.eth, 0); - assertEq(price.currency, USDC_PRICE); // 1 free, 1 paid - - // Purchase after using all free units (simulate 2 total purchases made) - vm.prank(address(PRODUCTS_MODULE)); - firstForFree.onProductPurchase(slicerId, productIds[0], 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)); - price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, buyer, ""); - assertEq(price.eth, 0); - assertEq(price.currency, USDC_PRICE); // All 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); } function testProductPrice_ERC721Condition() public { @@ -226,8 +225,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[0], + variantId, abi.encode( - USDC_PRICE, // usdcPrice erc721Conditions, // eligibleTokens address(0), // mintToken uint88(0), // mintTokenId @@ -238,27 +237,25 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { vm.stopPrank(); // Buyer without required token - should pay full price - Price memory price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, buyer, ""); - assertEq(price.eth, 0); - assertEq(price.currency, 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 - price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, buyer, ""); - assertEq(price.eth, 0); - assertEq(price.currency, 0); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, buyer, ""); + assertEq(price, 0); // Second free purchase - price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, buyer, ""); - assertEq(price.eth, 0); - assertEq(price.currency, 0); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, buyer, ""); + assertEq(price, 0); // Purchase exceeding free units - price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 3, buyer, ""); - assertEq(price.eth, 0); - assertEq(price.currency, 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 { @@ -276,8 +273,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[0], + variantId, abi.encode( - USDC_PRICE, // usdcPrice erc1155Conditions, // eligibleTokens address(0), // mintToken uint88(0), // mintTokenId @@ -289,23 +286,22 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { // Buyer without sufficient tokens - should pay full price mockERC1155.setBalance(buyer, 5, 5); // Less than required - Price memory price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, buyer, ""); - assertEq(price.eth, 0); - assertEq(price.currency, 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 - price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 2, buyer, ""); - assertEq(price.eth, 0); - assertEq(price.currency, 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 - price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, buyer, ""); - assertEq(price.eth, 0); - assertEq(price.currency, 0); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, buyer, ""); + assertEq(price, 0); } function testProductPrice_MultipleConditions() public { @@ -325,8 +321,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[0], + variantId, abi.encode( - USDC_PRICE, // usdcPrice multipleConditions, // eligibleTokens address(0), // mintToken uint88(0), // mintTokenId @@ -336,29 +332,27 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { vm.stopPrank(); + uint256 basePrice = _basePrice(slicerId, productIds[0], 1); + // Buyer meets first condition only mockERC721.mint(buyer); - Price memory price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, buyer, ""); - assertEq(price.eth, 0); - assertEq(price.currency, 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); - price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, buyer2, ""); - assertEq(price.eth, 0); - assertEq(price.currency, 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); - price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, buyer3, ""); - assertEq(price.eth, 0); - assertEq(price.currency, 0); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, buyer3, ""); + assertEq(price, 0); // Buyer meets neither condition - price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, address(0x999), ""); - assertEq(price.eth, 0); - assertEq(price.currency, USDC_PRICE); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, basePrice, 1, address(0x999), ""); + assertEq(price, basePrice); } function testOnProductPurchase_WithMinting() public { @@ -368,8 +362,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[0], + variantId, abi.encode( - USDC_PRICE, // usdcPrice noConditions, // eligibleTokens address(mockMintToken), // mintToken uint88(5), // mintTokenId @@ -379,33 +373,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], 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], 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], 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 { @@ -415,8 +402,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 @@ -424,13 +411,10 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { ) ); - assertEq(firstForFree.purchases(buyer, slicerId), 0); - vm.prank(address(PRODUCTS_MODULE)); - firstForFree.onProductPurchase(slicerId, productIds[0], 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); } @@ -443,35 +427,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], 0, buyer, 2, ""); - - // Purchase on slicer 1 - vm.prank(address(PRODUCTS_MODULE)); - firstForFree.onProductPurchase(1, productIds[0], 0, buyer, 1, ""); - - // Check separate tracking - assertEq(firstForFree.purchases(buyer, 0), 2); - assertEq(firstForFree.purchases(buyer, 1), 1); - // Verify pricing considers separate tracking - Price memory price = firstForFree.productPrice(0, productIds[0], 0, address(0), 1, buyer, ""); - assertEq(price.currency, USDC_PRICE); // No free units left on slicer 0 - - price = firstForFree.productPrice(1, productIds[0], 0, address(0), 1, buyer, ""); - assertEq(price.currency, 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 { @@ -481,8 +467,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[0], + variantId, abi.encode( - USDC_PRICE, // usdcPrice noConditions, // eligibleTokens address(0), // mintToken uint88(0), // mintTokenId @@ -491,14 +477,13 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { ); // Zero free units - should always pay - Price memory price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 1, buyer, ""); - assertEq(price.eth, 0); - assertEq(price.currency, 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 - price = firstForFree.productPrice(slicerId, productIds[0], 0, address(0), 0, buyer, ""); - assertEq(price.eth, 0); - assertEq(price.currency, 0); + price = firstForFree.productPrice(slicerId, productIds[0], variantId, ETH, 0, 0, buyer, ""); + assertEq(price, 0); } function testConfigureProduct_UpdateExisting() public { @@ -512,8 +497,8 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { firstForFree.configureProduct( slicerId, productIds[0], + variantId, abi.encode( - USDC_PRICE, // usdcPrice initialConditions, // eligibleTokens address(mockMintToken), // mintToken uint88(1), // mintTokenId @@ -535,8 +520,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 @@ -547,9 +532,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/HookRegistryTest.sol b/test/utils/HookRegistryTest.sol index 0085e77..e688331 100644 --- a/test/utils/HookRegistryTest.sol +++ b/test/utils/HookRegistryTest.sol @@ -10,7 +10,7 @@ bytes32 constant PRODUCT_MANAGER_ROLE = bytes32(uint256(0x04)); abstract contract HookRegistryTest is HookTest { function testParamsSchema() public view { - string memory schema = IHookRegistry(hook).paramsSchema(); + string memory schema = IHookRegistry(hook).configureProductSchema(); assertTrue(bytes(schema).length > 0); } @@ -22,10 +22,10 @@ abstract contract HookRegistryTest is HookTest { ) ); - IHookRegistry(hook).configureProduct(0, 0, ""); + IHookRegistry(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/mocks/MockProductsModule.sol b/test/utils/mocks/MockProductsModule.sol index 923ba7f..2ea1328 100644 --- a/test/utils/mocks/MockProductsModule.sol +++ b/test/utils/mocks/MockProductsModule.sol @@ -20,13 +20,32 @@ contract MockProductsModule is isAllowed = account == vm.addr(uint256(keccak256(abi.encodePacked("productOwner")))); } - function basePrice(uint256, uint256, uint256, address, uint256) external pure returns (Price memory price) { - price.eth = 1e16; - price.currency = 100e18; + function basePrice(uint256, uint256, uint256, address currency, uint256 quantity) + external + pure + returns (uint256 amount) + { + if (currency == address(0)) { + amount = 1e16 * quantity; + } else { + // USDC + amount = 100e6 * quantity; + } } 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 {} } From ff119334c1f416964998d584cbdcc83d6a1d08f3 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 29 Sep 2025 14:51:21 +0200 Subject: [PATCH 12/30] fix: NFTGated hook --- src/hooks/actions/NFTGated/NFTGated.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hooks/actions/NFTGated/NFTGated.sol b/src/hooks/actions/NFTGated/NFTGated.sol index c7d020b..5ba1ce7 100644 --- a/src/hooks/actions/NFTGated/NFTGated.sol +++ b/src/hooks/actions/NFTGated/NFTGated.sol @@ -77,13 +77,13 @@ contract NFTGated is RegistryProductAction { internal override { - (NFTGates memory nftGates_) = abi.decode(params, (NFTGates)); + (NFTGate[] memory gates, uint256 minOwned) = abi.decode(params, (NFTGate[], uint256)); delete nftGates[slicerId][productId][variantId].gates; - nftGates[slicerId][productId][variantId].minOwned = nftGates_.minOwned; - for (uint256 i = 0; i < nftGates_.gates.length; i++) { - nftGates[slicerId][productId][variantId].gates.push(nftGates_.gates[i]); + nftGates[slicerId][productId][variantId].minOwned = minOwned; + for (uint256 i = 0; i < gates.length; i++) { + nftGates[slicerId][productId][variantId].gates.push(gates[i]); } } @@ -91,6 +91,6 @@ contract NFTGated is RegistryProductAction { * @inheritdoc IHookRegistry */ function configureProductSchema() external pure override returns (string memory) { - return "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned"; + return "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] gates,uint256 minOwned"; } } From df8dfb9b360971f5af3f02841c75078ddb531bd8 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 29 Sep 2025 15:35:25 +0200 Subject: [PATCH 13/30] update hooks tests --- script/Deploy.s.sol | 1 - test/actions/Allowlisted/Allowlisted.t.sol | 34 ++++++++ test/actions/ERC20Gated/ERC20Gated.t.sol | 33 +++++++ test/actions/ERC20Mint/ERC20Mint.t.sol | 60 ++++++++++++- test/actions/ERC721Mint/ERC721Mint.t.sol | 60 ++++++++++++- test/actions/NFTGated/NFTGated.t.sol | 45 ++++++++-- test/pricing/TieredDiscount/NFTDiscount.t.sol | 1 - test/pricing/VRGDA/LogisticVRGDA.t.sol | 2 - .../correctness/LinearVRGDACorrectness.t.sol | 4 - .../FirstForFree/FirstForFree.t.sol | 86 ++++++++++++++++++- 10 files changed, 302 insertions(+), 24 deletions(-) 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/test/actions/Allowlisted/Allowlisted.t.sol b/test/actions/Allowlisted/Allowlisted.t.sol index e0d4daf..9418a75 100644 --- a/test/actions/Allowlisted/Allowlisted.t.sol +++ b/test/actions/Allowlisted/Allowlisted.t.sol @@ -8,6 +8,7 @@ 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; @@ -54,4 +55,37 @@ contract AllowlistedTest is RegistryProductActionTest { bytes32[] memory wrongProof = m.getProof(data, 1); 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 424c190..08e0eb5 100644 --- a/test/actions/ERC20Gated/ERC20Gated.t.sol +++ b/test/actions/ERC20Gated/ERC20Gated.t.sol @@ -9,6 +9,7 @@ import {IERC20, MockERC20} from "@test/utils/mocks/MockERC20.sol"; uint256 constant slicerId = 0; uint256 constant productId = 1; uint256 constant variantId = 0; +uint256 constant variantIdAlt = 1; contract ERC20GatedTest is RegistryProductActionTest { MockERC20Gated erc20Gated; @@ -70,4 +71,36 @@ contract ERC20GatedTest is RegistryProductActionTest { token.mint(buyer, 100); 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); + } } diff --git a/test/actions/ERC20Mint/ERC20Mint.t.sol b/test/actions/ERC20Mint/ERC20Mint.t.sol index 9478e96..0997895 100644 --- a/test/actions/ERC20Mint/ERC20Mint.t.sol +++ b/test/actions/ERC20Mint/ERC20Mint.t.sol @@ -6,10 +6,9 @@ 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"; - uint256 constant slicerId = 0; uint256 constant variantId = 0; +uint256 constant variantIdAlt = 1; contract ERC20MintTest is RegistryProductActionTest { ERC20Mint erc20Mint; @@ -363,4 +362,61 @@ contract ERC20MintTest is RegistryProductActionTest { vm.prank(address(PRODUCTS_MODULE)); 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); + } } diff --git a/test/actions/ERC721Mint/ERC721Mint.t.sol b/test/actions/ERC721Mint/ERC721Mint.t.sol index f4b3729..c0df264 100644 --- a/test/actions/ERC721Mint/ERC721Mint.t.sol +++ b/test/actions/ERC721Mint/ERC721Mint.t.sol @@ -6,10 +6,9 @@ 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"; - uint256 constant slicerId = 0; uint256 constant variantId = 0; +uint256 constant variantIdAlt = 1; contract ERC721MintTest is RegistryProductActionTest { ERC721Mint erc721Mint; @@ -398,6 +397,63 @@ 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( diff --git a/test/actions/NFTGated/NFTGated.t.sol b/test/actions/NFTGated/NFTGated.t.sol index a0af4f7..c7a22ed 100644 --- a/test/actions/NFTGated/NFTGated.t.sol +++ b/test/actions/NFTGated/NFTGated.t.sol @@ -3,13 +3,14 @@ pragma solidity ^0.8.30; import {RegistryProductActionTest} from "@test/utils/RegistryProductActionTest.sol"; import {MockNFTGated} from "./mocks/MockNFTGated.sol"; -import {NFTGates, NFTGate, NftType} from "@/hooks/actions/NFTGated/NFTGated.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"; - uint256 constant slicerId = 0; +uint256 constant variantId = 0; +uint256 constant variantIdAlt = 1; contract NFTGatedTest is RegistryProductActionTest { MockNFTGated nftGated; @@ -17,7 +18,6 @@ contract NFTGatedTest is RegistryProductActionTest { MockERC1155 nft1155 = new MockERC1155(); uint256[] productIds = [1, 2, 3, 4]; - uint256 variantId = 0; function setUp() public { nftGated = new MockNFTGated(PRODUCTS_MODULE); @@ -29,7 +29,7 @@ contract NFTGatedTest is RegistryProductActionTest { vm.startPrank(productOwner); for (uint256 i = 0; i < productIds.length; i++) { - nftGated.configureProduct(slicerId, productIds[i], variantId, abi.encode(nftGates[i])); + nftGated.configureProduct(slicerId, productIds[i], variantId, encodeNFTGates(nftGates[i])); assertEq(nftGated.nftGates(slicerId, productIds[i], variantId), nftGates[i].minOwned); } vm.stopPrank(); @@ -40,12 +40,12 @@ contract NFTGatedTest is RegistryProductActionTest { vm.startPrank(productOwner); - nftGated.configureProduct(slicerId, productIds[2], variantId, abi.encode(nftGates[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], variantId, abi.encode(nftGates[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); @@ -57,7 +57,7 @@ contract NFTGatedTest is RegistryProductActionTest { vm.startPrank(productOwner); for (uint256 i = 0; i < productIds.length; i++) { - nftGated.configureProduct(slicerId, productIds[i], variantId, abi.encode(nftGates[i])); + nftGated.configureProduct(slicerId, productIds[i], variantId, encodeNFTGates(nftGates[i])); } vm.stopPrank(); @@ -86,6 +86,31 @@ contract NFTGatedTest is RegistryProductActionTest { 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, "")); + } + /*////////////////////////////////////////////////////////////// INTERNAL //////////////////////////////////////////////////////////////*/ @@ -118,4 +143,8 @@ 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); + } } diff --git a/test/pricing/TieredDiscount/NFTDiscount.t.sol b/test/pricing/TieredDiscount/NFTDiscount.t.sol index fb655cb..fa42872 100644 --- a/test/pricing/TieredDiscount/NFTDiscount.t.sol +++ b/test/pricing/TieredDiscount/NFTDiscount.t.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.30; import {RegistryProductPriceTest} from "@test/utils/RegistryProductPriceTest.sol"; -import {console2} from "forge-std/console2.sol"; import { IProductsModule, NFTDiscount, diff --git a/test/pricing/VRGDA/LogisticVRGDA.t.sol b/test/pricing/VRGDA/LogisticVRGDA.t.sol index 87a9e87..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; diff --git a/test/pricing/VRGDA/correctness/LinearVRGDACorrectness.t.sol b/test/pricing/VRGDA/correctness/LinearVRGDACorrectness.t.sol index bc87461..3f15239 100644 --- a/test/pricing/VRGDA/correctness/LinearVRGDACorrectness.t.sol +++ b/test/pricing/VRGDA/correctness/LinearVRGDACorrectness.t.sol @@ -1,7 +1,6 @@ // 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"; @@ -46,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 9e241b6..5e523b9 100644 --- a/test/pricingActions/FirstForFree/FirstForFree.t.sol +++ b/test/pricingActions/FirstForFree/FirstForFree.t.sol @@ -8,8 +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"; - contract MockERC1155Token is ITokenERC1155 { mapping(address => mapping(uint256 => uint256)) public balanceOf; mapping(address => uint256) public mintedAmounts; @@ -32,10 +30,19 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { uint256 slicerId = 0; uint256[] productIds = [1, 2, 3, 4]; - uint256 variantId = 0; + uint256 constant variantId = 0; + uint256 constant variantIdAlt = 1; function _basePrice(uint256 slicerId_, uint256 productId_, uint256 quantity) internal view returns (uint256) { - return PRODUCTS_MODULE.basePrice(slicerId_, productId_, variantId, ETH, quantity); + 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 { @@ -160,6 +167,31 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { 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); @@ -214,6 +246,52 @@ contract FirstForFreeTest is RegistryProductPriceActionTest { assertEq(price, basePrice); } + function testProductPrice_variantSpecificConfiguration() 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(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 { vm.startPrank(productOwner); From ce1851e9d1b93dccd1036c11c8fb3eb431a2060d Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 1 Oct 2025 02:55:53 +0200 Subject: [PATCH 14/30] indexer: update tests wip --- .../actions/BaseGirlsScout/BaseGirlsScout.sol | 76 ------------------- .../NFTDiscount/NFTDiscount.sol | 2 +- .../pricing/TieredDiscount/TieredDiscount.sol | 1 - .../LinearVRGDAPrices/LinearVRGDAPrices.sol | 1 - .../LogisticVRGDAPrices.sol | 1 - .../FirstForFree/FirstForFree.sol | 1 - test/pricing/TieredDiscount/NFTDiscount.t.sol | 1 - .../FirstForFree/FirstForFree.t.sol | 2 +- 8 files changed, 2 insertions(+), 83 deletions(-) delete mode 100644 src/custom/actions/BaseGirlsScout/BaseGirlsScout.sol diff --git a/src/custom/actions/BaseGirlsScout/BaseGirlsScout.sol b/src/custom/actions/BaseGirlsScout/BaseGirlsScout.sol deleted file mode 100644 index 9e59d48..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, uint256, address buyer, uint256 quantity, 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/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol index 7d81097..575e624 100644 --- a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol @@ -4,7 +4,7 @@ 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 {DiscountParams, Price, TieredDiscount} from "../TieredDiscount.sol"; +import {DiscountParams, TieredDiscount} from "../TieredDiscount.sol"; import {NFTType} from "../types/DiscountParams.sol"; /** diff --git a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol index 984886b..1d2a610 100644 --- a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.30; import {IProductsModule} from "slice/interfaces/IProductsModule.sol"; import {RegistryProductPrice, IProductPrice} from "@/utils/RegistryProductPrice.sol"; import {DiscountParams} from "./types/DiscountParams.sol"; -import {Price} from "slice/types/Price.sol"; /** * @title TieredDiscount diff --git a/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol index 2f6bf69..a1aa54b 100644 --- a/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol @@ -5,7 +5,6 @@ import {HookRegistry, IProductPrice, IHookRegistry, IProductsModule} from "@/uti import {wadLn, unsafeWadDiv, toDaysWadUnsafe} from "@/utils/math/SignedWadMath.sol"; import {LinearProduct} from "../types/LinearProduct.sol"; import {VRGDAPrices} from "../VRGDAPrices.sol"; -import {Price} from "slice/types/Price.sol"; /// @title LinearVRGDAPrices /// @notice VRGDA with a linear issuance curve - Price library with different params for each Slice product. diff --git a/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol b/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol index ce57c43..c4f1524 100644 --- a/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol @@ -14,7 +14,6 @@ import { } from "@/utils/math/SignedWadMath.sol"; import {LogisticProduct} from "../types/LogisticProduct.sol"; import {IProductsModule, VRGDAPrices} from "../VRGDAPrices.sol"; -import {Price} from "slice/types/Price.sol"; /// @title LogisticVRGDAPrices /// @notice VRGDA with a logistic issuance curve - Price library with different params for each Slice product. diff --git a/src/hooks/pricingActions/FirstForFree/FirstForFree.sol b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol index bf63c03..ab2dd9f 100644 --- a/src/hooks/pricingActions/FirstForFree/FirstForFree.sol +++ b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol @@ -14,7 +14,6 @@ import {HookRegistry} from "@/utils/RegistryProductAction.sol"; import {ProductParams, TokenCondition} from "./types/ProductParams.sol"; import {TokenType} from "./types/TokenCondition.sol"; import {ITokenERC1155} from "./utils/ITokenERC1155.sol"; -import {Price} from "slice/types/Price.sol"; /** * @title FirstForFree diff --git a/test/pricing/TieredDiscount/NFTDiscount.t.sol b/test/pricing/TieredDiscount/NFTDiscount.t.sol index fa42872..e172fd3 100644 --- a/test/pricing/TieredDiscount/NFTDiscount.t.sol +++ b/test/pricing/TieredDiscount/NFTDiscount.t.sol @@ -5,7 +5,6 @@ import {RegistryProductPriceTest} from "@test/utils/RegistryProductPriceTest.sol import { IProductsModule, NFTDiscount, - Price, DiscountParams, NFTType } from "@/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol"; diff --git a/test/pricingActions/FirstForFree/FirstForFree.t.sol b/test/pricingActions/FirstForFree/FirstForFree.t.sol index 5e523b9..6be2046 100644 --- a/test/pricingActions/FirstForFree/FirstForFree.t.sol +++ b/test/pricingActions/FirstForFree/FirstForFree.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.30; import {RegistryProductPriceActionTest} from "@test/utils/RegistryProductPriceActionTest.sol"; -import {Price, FirstForFree} from "@/hooks/pricingActions/FirstForFree/FirstForFree.sol"; +import {FirstForFree} from "@/hooks/pricingActions/FirstForFree/FirstForFree.sol"; import {ProductParams} from "@/hooks/pricingActions/FirstForFree/types/ProductParams.sol"; import {TokenCondition, TokenType} from "@/hooks/pricingActions/FirstForFree/types/TokenCondition.sol"; import {ITokenERC1155} from "@/hooks/pricingActions/FirstForFree/utils/ITokenERC1155.sol"; From 4f2236fb7c3be48f3a3a5c96ebb4eebb4068aa46 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Thu, 2 Oct 2025 15:25:31 +0200 Subject: [PATCH 15/30] indexer: add hooks tests --- deployments/addresses.json | 8 ++++---- src/hooks/pricingActions/FirstForFree/FirstForFree.sol | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/deployments/addresses.json b/deployments/addresses.json index c3a44b1..089f101 100644 --- a/deployments/addresses.json +++ b/deployments/addresses.json @@ -36,7 +36,7 @@ { "address": "0xD4eF7A46bF4c58036eaCA886119F5230e5a2C25d", "blockNumber": 33563508, - "paramsSchema": "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned", + "paramsSchema": "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] gates,uint256 minOwned", "transactionHash": "0x9dd101a6155b432849cb33f65c5d4b9f6875e6b1e7bf806005a8a6b0d070f634" } ] @@ -54,7 +54,7 @@ { "address": "0xEC68E30182F4298b7032400B7ce809da613e4449", "blockNumber": 33511188, - "paramsSchema": "(address currency,int128 targetPrice,uint128 min,int256 perTimeUnit)[] linearParams,int256 priceDecayPercent", + "paramsSchema": "int184 priceDecayPercent,uint16 min,int256 perTimeUnit", "transactionHash": "0xf2667ce20d07561e59c8d3e3de135bbd895c5b08bb57fe487c6c8197c91a0d73" } ], @@ -62,7 +62,7 @@ { "address": "0x2b02cC8528EF18abf8185543CEC29A94F0542c8F", "blockNumber": 33511209, - "paramsSchema": "(address currency,int128 targetPrice,uint128 min,int256 timeScale)[] logisticParams,int256 priceDecayPercent", + "paramsSchema": "int184 priceDecayPercent,uint16 min,int256 timescale", "transactionHash": "0x2b27380661a38b54dbccf634305622296034ddfccf5865a07fbfb7810ea41025" } ] @@ -72,7 +72,7 @@ { "address": "0x2C18D37b8229233F672bF406bCe8799BCfD43B5A", "blockNumber": 33510960, - "paramsSchema": "uint256 usdcPrice,(address tokenAddress,uint8 tokenType,uint88 tokenId,uint8 minQuantity)[] eligibleTokens,address mintToken,uint88 mintTokenId,uint8 freeUnits", + "paramsSchema": "(address tokenAddress,uint8 tokenType,uint80 tokenId,uint8 minQuantity)[] eligibleTokens,address mintToken,uint88 mintTokenId,uint8 freeUnits", "transactionHash": "0x8bd0b9de8edd704899210f8450096adf7f4f853030a7ba0a2f64494bc1ba726e" } ] diff --git a/src/hooks/pricingActions/FirstForFree/FirstForFree.sol b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol index ab2dd9f..6c825dd 100644 --- a/src/hooks/pricingActions/FirstForFree/FirstForFree.sol +++ b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol @@ -124,7 +124,7 @@ contract FirstForFree is RegistryProductPriceAction { */ function configureProductSchema() external pure override returns (string memory) { return - "(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"; } /*////////////////////////////////////////////////////////////// From 91de9881c53325da7416833a8602612f0d3d1d18 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Thu, 2 Oct 2025 21:23:33 +0200 Subject: [PATCH 16/30] indexer: final --- script/Seed.s.sol | 2 +- src/hooks/actions/NFTGated/NFTGated.sol | 9 +++++++++ test/actions/NFTGated/NFTGated.t.sol | 27 ++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/script/Seed.s.sol b/script/Seed.s.sol index 8754d81..8ba86be 100644 --- a/script/Seed.s.sol +++ b/script/Seed.s.sol @@ -57,7 +57,7 @@ contract SeedHooksScript is Script { code: address(new LogisticVRGDAPrices(Contracts.PRODUCTS_MODULE)).code }); hooks[8] = Hook({ - hookAddress: 0xEC68E30182F4298b7032400B7ce809da613e4449, + hookAddress: 0x2C18D37b8229233F672bF406bCe8799BCfD43B5A, code: address(new FirstForFree(Contracts.PRODUCTS_MODULE)).code }); diff --git a/src/hooks/actions/NFTGated/NFTGated.sol b/src/hooks/actions/NFTGated/NFTGated.sol index 5ba1ce7..916bc97 100644 --- a/src/hooks/actions/NFTGated/NFTGated.sol +++ b/src/hooks/actions/NFTGated/NFTGated.sol @@ -18,6 +18,8 @@ import {NftType, NFTGate, NFTGates} from "./types/NFTGate.sol"; * @author Slice */ contract NFTGated is RegistryProductAction { + error InvalidNFT(); + /*////////////////////////////////////////////////////////////// MUTABLE STORAGE //////////////////////////////////////////////////////////////*/ @@ -83,6 +85,13 @@ contract NFTGated is RegistryProductAction { nftGates[slicerId][productId][variantId].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(); + } nftGates[slicerId][productId][variantId].gates.push(gates[i]); } } diff --git a/test/actions/NFTGated/NFTGated.t.sol b/test/actions/NFTGated/NFTGated.t.sol index c7a22ed..72c39c3 100644 --- a/test/actions/NFTGated/NFTGated.t.sol +++ b/test/actions/NFTGated/NFTGated.t.sol @@ -1,8 +1,9 @@ // 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 {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"; @@ -111,6 +112,22 @@ contract NFTGatedTest is RegistryProductActionTest { 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(); + } + /*////////////////////////////////////////////////////////////// INTERNAL //////////////////////////////////////////////////////////////*/ @@ -148,3 +165,11 @@ contract NFTGatedTest is RegistryProductActionTest { return abi.encode(nftGates_.gates, nftGates_.minOwned); } } + +contract TestERC20 is ERC20 { + constructor() ERC20("Test", "TST") {} + + function supportsInterface(bytes4 interfaceId) public pure returns (bool) { + return false; + } +} From e06a7e47209e74892d941cae6bef599cbbce4505 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Thu, 2 Oct 2025 21:53:08 +0200 Subject: [PATCH 17/30] fixes --- test/actions/NFTGated/NFTGated.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/actions/NFTGated/NFTGated.t.sol b/test/actions/NFTGated/NFTGated.t.sol index 72c39c3..54278fa 100644 --- a/test/actions/NFTGated/NFTGated.t.sol +++ b/test/actions/NFTGated/NFTGated.t.sol @@ -169,7 +169,7 @@ contract NFTGatedTest is RegistryProductActionTest { contract TestERC20 is ERC20 { constructor() ERC20("Test", "TST") {} - function supportsInterface(bytes4 interfaceId) public pure returns (bool) { + function supportsInterface(bytes4) public pure returns (bool) { return false; } } From 0c139143e1e63b85a76f667d83b1fcabed612a49 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 5 Oct 2025 01:00:04 +0200 Subject: [PATCH 18/30] update hooks contracts with support for new interfaces --- script/ScriptUtils.sol | 24 +++-- src/hooks/actions/Allowlisted/Allowlisted.sol | 10 +-- src/hooks/actions/ERC20Gated/ERC20Gated.sol | 10 +-- src/hooks/actions/ERC20Mint/ERC20Mint.sol | 10 +-- src/hooks/actions/ERC721Mint/ERC721Mint.sol | 13 ++- src/hooks/actions/NFTGated/NFTGated.sol | 10 +-- .../NFTDiscount/NFTDiscount.sol | 88 ++++++++++++------- .../pricing/TieredDiscount/TieredDiscount.sol | 68 +++++++++----- .../LinearVRGDAPrices/LinearVRGDAPrices.sol | 10 ++- .../LogisticVRGDAPrices.sol | 10 ++- .../FirstForFree/FirstForFree.sol | 10 +-- src/interfaces/IHookRegistry.sol | 2 +- src/interfaces/IProductPrice.sol | 2 +- src/utils/RegistryPrice.sol | 4 + ...ryTest.sol => ProductHookRegistryTest.sol} | 8 +- test/utils/RegistryProductActionTest.sol | 8 +- test/utils/RegistryProductPriceActionTest.sol | 8 +- test/utils/RegistryProductPriceTest.sol | 8 +- 18 files changed, 187 insertions(+), 116 deletions(-) create mode 100644 src/utils/RegistryPrice.sol rename test/utils/{HookRegistryTest.sol => ProductHookRegistryTest.sol} (82%) diff --git a/script/ScriptUtils.sol b/script/ScriptUtils.sol index 82f8294..67984b4 100644 --- a/script/ScriptUtils.sol +++ b/script/ScriptUtils.sol @@ -7,7 +7,7 @@ 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} from "slice/interfaces/hooks/IHookRegistry/IProductHookRegistry.sol"; /** * Helper contract to enforce correct chain selection in scripts @@ -85,7 +85,8 @@ 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; bytes32 transactionHash; } @@ -136,7 +137,9 @@ abstract contract SetUpContractsList is Script { 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); + vm.serializeString( + idx, "configureProductSchema", existingData[j].configureProductSchema + ); arrStrings[j] = vm.serializeBytes32(idx, "transactionHash", existingData[j].transactionHash); } @@ -213,7 +216,12 @@ abstract contract SetUpContractsList is Script { 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).configureProductSchema()); + vm.serializeString( + "0", + "configureProductSchema", + IProductHookRegistry(transaction.contractAddress).configureProductSchema() + ); + // TODO: Add other params to the json? such as configure[ProductType/Slicer]Schema and [product/productType/slicer]PriceSchema json[0] = vm.serializeBytes32("0", "transactionHash", transaction.hash); for (uint256 i = 0; i < existingContractAddresses.length; i++) { @@ -222,14 +230,18 @@ abstract contract SetUpContractsList is Script { vm.serializeAddress(index, "address", existingContractAddress.contractAddress); vm.serializeUint(index, "blockNumber", existingContractAddress.blockNumber); - vm.serializeString(index, "paramsSchema", existingContractAddress.paramsSchema); + vm.serializeString(index, "configureProductSchema", existingContractAddress.configureProductSchema); 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).configureProductSchema()); + vm.serializeString( + "0", + "configureProductSchema", + IProductHookRegistry(transaction.contractAddress).configureProductSchema() + ); json[0] = vm.serializeBytes32("0", "transactionHash", transaction.hash); } } diff --git a/src/hooks/actions/Allowlisted/Allowlisted.sol b/src/hooks/actions/Allowlisted/Allowlisted.sol index acf1dfc..eb722e3 100644 --- a/src/hooks/actions/Allowlisted/Allowlisted.sol +++ b/src/hooks/actions/Allowlisted/Allowlisted.sol @@ -5,9 +5,9 @@ import {MerkleProof} from "@openzeppelin-4.8.0/utils/cryptography/MerkleProof.so import { IProductsModule, RegistryProductAction, - HookRegistry, + ProductHookRegistry, IProductAction, - IHookRegistry + IProductHookRegistry } from "@/utils/RegistryProductAction.sol"; /** @@ -65,7 +65,7 @@ contract Allowlisted is RegistryProductAction { } /** - * @inheritdoc HookRegistry + * @inheritdoc ProductHookRegistry * @dev Sets the Merkle root for the allowlist. */ function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) @@ -77,9 +77,9 @@ contract Allowlisted is RegistryProductAction { } /** - * @inheritdoc IHookRegistry + * @inheritdoc IProductHookRegistry */ - function configureProductSchema() 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 b05d06e..dfedaf1 100644 --- a/src/hooks/actions/ERC20Gated/ERC20Gated.sol +++ b/src/hooks/actions/ERC20Gated/ERC20Gated.sol @@ -5,9 +5,9 @@ import {ERC20Gate} from "./types/ERC20Gate.sol"; import { IProductsModule, RegistryProductAction, - HookRegistry, + ProductHookRegistry, IProductAction, - IHookRegistry + IProductHookRegistry } from "@/utils/RegistryProductAction.sol"; /** @@ -59,7 +59,7 @@ contract ERC20Gated is RegistryProductAction { } /** - * @inheritdoc HookRegistry + * @inheritdoc ProductHookRegistry * @dev Sets the ERC20 gates for a product. */ function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) @@ -76,9 +76,9 @@ contract ERC20Gated is RegistryProductAction { } /** - * @inheritdoc IHookRegistry + * @inheritdoc IProductHookRegistry */ - function configureProductSchema() external pure override returns (string memory) { + function configureProductSchema() public pure override returns (string memory) { return "(address erc20,uint256 amount)[] erc20Gates"; } } diff --git a/src/hooks/actions/ERC20Mint/ERC20Mint.sol b/src/hooks/actions/ERC20Mint/ERC20Mint.sol index 7909f12..77d279d 100644 --- a/src/hooks/actions/ERC20Mint/ERC20Mint.sol +++ b/src/hooks/actions/ERC20Mint/ERC20Mint.sol @@ -4,9 +4,9 @@ pragma solidity ^0.8.30; import { IProductsModule, RegistryProductAction, - HookRegistry, + ProductHookRegistry, IProductAction, - IHookRegistry + IProductHookRegistry } from "@/utils/RegistryProductAction.sol"; import {ERC20Data} from "./types/ERC20Data.sol"; import {ERC20Mint_BaseToken} from "./utils/ERC20Mint_BaseToken.sol"; @@ -89,7 +89,7 @@ contract ERC20Mint is RegistryProductAction { } /** - * @inheritdoc HookRegistry + * @inheritdoc ProductHookRegistry * @dev Set the ERC20 data for a product. */ function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) @@ -123,9 +123,9 @@ contract ERC20Mint is RegistryProductAction { } /** - * @inheritdoc IHookRegistry + * @inheritdoc IProductHookRegistry */ - function configureProductSchema() external pure override returns (string memory) { + function configureProductSchema() public pure override returns (string memory) { return "string name,string symbol,uint256 premintAmount,address premintReceiver,bool revertOnMaxSupplyReached,uint256 maxSupply,uint256 tokensPerUnit"; } diff --git a/src/hooks/actions/ERC721Mint/ERC721Mint.sol b/src/hooks/actions/ERC721Mint/ERC721Mint.sol index dbf1db1..ad697f0 100644 --- a/src/hooks/actions/ERC721Mint/ERC721Mint.sol +++ b/src/hooks/actions/ERC721Mint/ERC721Mint.sol @@ -1,7 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import {IProductsModule, RegistryProductAction, HookRegistry, IHookRegistry} from "@/utils/RegistryProductAction.sol"; +import { + IProductsModule, + RegistryProductAction, + ProductHookRegistry, + IProductHookRegistry +} from "@/utils/RegistryProductAction.sol"; import {MAX_ROYALTY, ERC721Mint_BaseToken} from "./utils/ERC721Mint_BaseToken.sol"; import {ERC721Data} from "./types/ERC721Data.sol"; @@ -60,7 +65,7 @@ contract ERC721Mint is RegistryProductAction { } /** - * @inheritdoc HookRegistry + * @inheritdoc ProductHookRegistry * @dev Set the ERC721 data for a product. */ function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) @@ -94,9 +99,9 @@ contract ERC721Mint is RegistryProductAction { } /** - * @inheritdoc IHookRegistry + * @inheritdoc IProductHookRegistry */ - function configureProductSchema() external pure override returns (string memory) { + 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"; } diff --git a/src/hooks/actions/NFTGated/NFTGated.sol b/src/hooks/actions/NFTGated/NFTGated.sol index 916bc97..b832671 100644 --- a/src/hooks/actions/NFTGated/NFTGated.sol +++ b/src/hooks/actions/NFTGated/NFTGated.sol @@ -6,9 +6,9 @@ import {IERC1155} from "@openzeppelin-4.8.0/interfaces/IERC1155.sol"; import { IProductsModule, RegistryProductAction, - HookRegistry, + ProductHookRegistry, IProductAction, - IHookRegistry + IProductHookRegistry } from "@/utils/RegistryProductAction.sol"; import {NftType, NFTGate, NFTGates} from "./types/NFTGate.sol"; @@ -72,7 +72,7 @@ contract NFTGated is RegistryProductAction { } /** - * @inheritdoc HookRegistry + * @inheritdoc ProductHookRegistry * @dev Set the NFT gates for a product. */ function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) @@ -97,9 +97,9 @@ contract NFTGated is RegistryProductAction { } /** - * @inheritdoc IHookRegistry + * @inheritdoc IProductHookRegistry */ - function configureProductSchema() external pure override returns (string memory) { + function configureProductSchema() public pure override returns (string memory) { return "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] gates,uint256 minOwned"; } } diff --git a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol index 575e624..6ad8410 100644 --- a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol @@ -3,7 +3,14 @@ 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, + ProductHookRegistry, + IProductHookRegistry, + IProductTypeHookRegistry, + ISlicerHookRegistry, + IProductsModule +} from "@/utils/RegistryPrice.sol"; import {DiscountParams, TieredDiscount} from "../TieredDiscount.sol"; import {NFTType} from "../types/DiscountParams.sol"; @@ -23,21 +30,9 @@ contract NFTDiscount is TieredDiscount { CONFIGURATION //////////////////////////////////////////////////////////////*/ - /** - * @inheritdoc HookRegistry - * @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, uint256 variantId, bytes memory params) - internal - override - { + function setProductDiscounts(DiscountParams[] storage storageDiscounts, bytes memory params) internal { (DiscountParams[] memory newDiscounts) = abi.decode(params, (DiscountParams[])); - DiscountParams[] storage productDiscount = discounts[slicerId][productId][variantId]; - - delete discounts[slicerId][productId][variantId]; - uint256 prevDiscountValue; DiscountParams memory discountParam; for (uint256 i; i < newDiscounts.length;) { @@ -60,7 +55,7 @@ contract NFTDiscount is TieredDiscount { } prevDiscountValue = discountParam.discount; - productDiscount.push(discountParam); + storageDiscounts.push(discountParam); unchecked { ++i; @@ -69,36 +64,65 @@ contract NFTDiscount is TieredDiscount { } /** - * @inheritdoc IHookRegistry + * @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, uint256 variantId, bytes memory params) + internal + override + { + delete discounts[slicerId][productId][variantId]; + setProductDiscounts(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 configureProductSchema() external pure override returns (string memory) { + function _configureProductType(uint256 slicerId, uint256 productTypeId, bytes memory params) internal override { + delete productTypeDiscounts[slicerId][productTypeId]; + setProductDiscounts(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]; + setProductDiscounts(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"; } + /*////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////*/ + /** * @inheritdoc TieredDiscount * @notice Base price is returned if user does not have a discount. */ - function _productPrice( - uint256, - uint256, - uint256, - address, - uint256, - address buyer, - bytes memory, - uint256 basePriceAmount, - DiscountParams[] memory discountParams - ) internal view virtual override returns (uint256 price) { + function _productPrice(address buyer, uint256 basePriceAmount, DiscountParams[] memory discountParams) + internal + view + virtual + override + returns (uint256 price) + { uint256 discount = _getHighestDiscount(discountParams, buyer); price = discount != 0 ? _getDiscountedPrice(basePriceAmount, discount) : basePriceAmount; } - /*////////////////////////////////////////////////////////////// - INTERNAL - //////////////////////////////////////////////////////////////*/ - /** * @notice Gets the highest discount available for a user, based on owned NFTs. * diff --git a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol index 1d2a610..d26c337 100644 --- a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.30; import {IProductsModule} from "slice/interfaces/IProductsModule.sol"; -import {RegistryProductPrice, IProductPrice} from "@/utils/RegistryProductPrice.sol"; +import {RegistryPrice, IProductPrice, IProductTypePrice, ISlicerProductPrice} from "@/utils/RegistryPrice.sol"; import {DiscountParams} from "./types/DiscountParams.sol"; /** @@ -10,7 +10,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 //////////////////////////////////////////////////////////////*/ @@ -26,12 +26,14 @@ abstract contract TieredDiscount is RegistryProductPrice { 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 @@ -44,15 +46,46 @@ abstract contract TieredDiscount is RegistryProductPrice { uint256 slicerId, uint256 productId, uint256 variantId, - address currency, + address, uint256 basePrice, - uint256 quantity, + uint256, address buyer, - bytes memory data + bytes memory ) public view override returns (uint256) { DiscountParams[] memory discountParams = discounts[slicerId][productId][variantId]; - return _productPrice(slicerId, productId, variantId, currency, quantity, buyer, data, basePrice, discountParams); + return _productPrice(buyer, basePrice, discountParams); + } + + /** + * @inheritdoc IProductTypePrice + */ + function productTypePrice( + uint256 slicerId, + uint256 productTypeId, + address, + uint256 basePrice, + uint256, + address buyer, + bytes memory + ) public view override returns (uint256) { + DiscountParams[] memory discountParams = productTypeDiscounts[slicerId][productTypeId]; + + return _productPrice(buyer, basePrice, discountParams); + } + + /** + * @inheritdoc ISlicerProductPrice + */ + function slicerProductPrice(uint256 slicerId, address, uint256 basePrice, uint256, address buyer, bytes memory) + public + view + override + returns (uint256) + { + DiscountParams[] memory discountParams = slicerDiscounts[slicerId]; + + return _productPrice(buyer, basePrice, discountParams); } /*////////////////////////////////////////////////////////////// @@ -62,26 +95,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 in selected currency. * @param discountParams Array of discount parameters. * * @return Price of product in selected currency. */ - function _productPrice( - uint256 slicerId, - uint256 productId, - uint256 variantId, - address currency, - uint256 quantity, - address buyer, - bytes memory data, - uint256 basePrice, - DiscountParams[] memory discountParams - ) internal view virtual returns (uint256); + 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 a1aa54b..adba315 100644 --- a/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol @@ -1,7 +1,9 @@ // 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 {LinearProduct} from "../types/LinearProduct.sol"; import {VRGDAPrices} from "../VRGDAPrices.sol"; @@ -29,7 +31,7 @@ contract LinearVRGDAPrices is VRGDAPrices { //////////////////////////////////////////////////////////////*/ /** - * @inheritdoc HookRegistry + * @inheritdoc ProductHookRegistry * @notice Set LinearProduct for a product. */ function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) @@ -95,9 +97,9 @@ contract LinearVRGDAPrices is VRGDAPrices { } /** - * @inheritdoc IHookRegistry + * @inheritdoc IProductHookRegistry */ - function configureProductSchema() external pure override returns (string memory) { + 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 c4f1524..8dcbb5c 100644 --- a/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol @@ -1,7 +1,9 @@ // 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, @@ -38,7 +40,7 @@ contract LogisticVRGDAPrices is VRGDAPrices { //////////////////////////////////////////////////////////////*/ /** - * @inheritdoc HookRegistry + * @inheritdoc ProductHookRegistry * @notice Set LogisticVRGDAParams for a product. */ function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) @@ -105,9 +107,9 @@ contract LogisticVRGDAPrices is VRGDAPrices { } /** - * @inheritdoc IHookRegistry + * @inheritdoc IProductHookRegistry */ - function configureProductSchema() external pure override returns (string memory) { + function configureProductSchema() public pure override returns (string memory) { return "int184 priceDecayPercent,uint16 min,int256 timescale"; } diff --git a/src/hooks/pricingActions/FirstForFree/FirstForFree.sol b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol index 6c825dd..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"; @@ -91,7 +91,7 @@ contract FirstForFree is RegistryProductPriceAction { } /** - * @inheritdoc HookRegistry + * @inheritdoc ProductHookRegistry * @notice Sets the product parameters. */ function _configureProduct(uint256 slicerId, uint256 productId, uint256 variantId, bytes memory params) @@ -120,9 +120,9 @@ contract FirstForFree is RegistryProductPriceAction { } /** - * @inheritdoc IHookRegistry + * @inheritdoc IProductHookRegistry */ - function configureProductSchema() external pure override returns (string memory) { + function configureProductSchema() public pure override returns (string memory) { return "(address tokenAddress,uint8 tokenType,uint80 tokenId,uint8 minQuantity)[] eligibleTokens,address mintToken,uint88 mintTokenId,uint8 freeUnits"; } 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/IProductPrice.sol b/src/interfaces/IProductPrice.sol index 0b4afc9..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 {IProductPrice} from "slice/interfaces.sol"; +import {IProductPrice, IProductTypePrice, ISlicerProductPrice} from "slice/interfaces.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/utils/HookRegistryTest.sol b/test/utils/ProductHookRegistryTest.sol similarity index 82% rename from test/utils/HookRegistryTest.sol rename to test/utils/ProductHookRegistryTest.sol index e688331..ec3340b 100644 --- a/test/utils/HookRegistryTest.sol +++ b/test/utils/ProductHookRegistryTest.sol @@ -2,15 +2,15 @@ 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"; bytes32 constant PRODUCT_MANAGER_ROLE = bytes32(uint256(0x04)); -abstract contract HookRegistryTest is HookTest { +abstract contract ProductHookRegistryTest is HookTest { function testParamsSchema() public view { - string memory schema = IHookRegistry(hook).configureProductSchema(); + string memory schema = IProductHookRegistry(hook).configureProductSchema(); assertTrue(bytes(schema).length > 0); } @@ -22,7 +22,7 @@ abstract contract HookRegistryTest is HookTest { ) ); - IHookRegistry(hook).configureProduct(0, 0, 0, ""); + IProductHookRegistry(hook).configureProduct(0, 0, 0, ""); } // TODO: verify configureProductSchema effectively corresponds to the params diff --git a/test/utils/RegistryProductActionTest.sol b/test/utils/RegistryProductActionTest.sol index 3f45593..2adbbf3 100644 --- a/test/utils/RegistryProductActionTest.sol +++ b/test/utils/RegistryProductActionTest.sol @@ -1,13 +1,13 @@ // 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 { diff --git a/test/utils/RegistryProductPriceActionTest.sol b/test/utils/RegistryProductPriceActionTest.sol index 118724e..7b3cbd6 100644 --- a/test/utils/RegistryProductPriceActionTest.sol +++ b/test/utils/RegistryProductPriceActionTest.sol @@ -1,20 +1,20 @@ // 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 { 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)); } } From 7a3e3a20404cf513bec297f67f89453c5797a455 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 5 Oct 2025 02:12:10 +0200 Subject: [PATCH 19/30] fixes --- deployments/addresses.json | 44 +++--- script/Seed.s.sol | 19 +-- script/generate-hook.sh | 134 ++++++++++-------- src/hooks/actions/Allowlisted/Allowlisted.sol | 2 +- .../NFTDiscount/NFTDiscount.sol | 10 +- test/utils/ProductHookRegistryTest.sol | 2 +- 6 files changed, 114 insertions(+), 97 deletions(-) diff --git a/deployments/addresses.json b/deployments/addresses.json index 089f101..abcdb84 100644 --- a/deployments/addresses.json +++ b/deployments/addresses.json @@ -2,17 +2,17 @@ "actions": { "Allowlisted": [ { - "address": "0x157428DD791E03c20880D22C3dA2B66A36B5cF26", - "blockNumber": 33510607, - "paramsSchema": "bytes32 merkleRoot", - "transactionHash": "0x6af9ac700f1c9a38de57fa4bc13262162d9b674649fd417ccd50237b6cfbc178" + "address": "0x57c6Ae75B3903755ADb1E1140d80b91FE12394D6", + "blockNumber": 36415274, + "configureProductSchema": "bytes32 merkleRoot", + "transactionHash": "0xe1c603de0844e496a13bfb4996f6436b198a788aedfd384d227b1f85b09c6b4a" } ], "ERC20Gated": [ { "address": "0x26A1C86B555013995Fc72864D261fDe984752E7c", "blockNumber": 33558792, - "paramsSchema": "(address erc20,uint256 amount)[] erc20Gates", + "configureProductSchema": "(address erc20,uint256 amount)[] erc20Gates", "transactionHash": "0x3a7c01ede05a34280073479d5cdf1f35e41d9f08c36a71f358b9c503ccc54526" } ], @@ -20,7 +20,7 @@ { "address": "0x67f9799FaC1D53C63217BEE47f553150F5BB0836", "blockNumber": 33520592, - "paramsSchema": "string name,string symbol,uint256 premintAmount,address premintReceiver,bool revertOnMaxSupplyReached,uint256 maxSupply,uint256 tokensPerUnit", + "configureProductSchema": "string name,string symbol,uint256 premintAmount,address premintReceiver,bool revertOnMaxSupplyReached,uint256 maxSupply,uint256 tokensPerUnit", "transactionHash": "0xfcd7e8fe47aa509afa0acdef86b741656c2602626f3258f8a83b359c665d6b04" } ], @@ -28,7 +28,7 @@ { "address": "0x2b6488115FAa50142E140172CbCd60e6370675F7", "blockNumber": 33511082, - "paramsSchema": "string name,string symbol,address royaltyReceiver,uint256 royaltyFraction,string baseURI,string tokenURI,bool revertOnMaxSupplyReached,uint256 maxSupply", + "configureProductSchema": "string name,string symbol,address royaltyReceiver,uint256 royaltyFraction,string baseURI,string tokenURI,bool revertOnMaxSupplyReached,uint256 maxSupply", "transactionHash": "0x4981b3b67c0b8abe6a942b3ca86643f2b1dfdbcce59d6c819ad20a82814956e0" } ], @@ -36,25 +36,17 @@ { "address": "0xD4eF7A46bF4c58036eaCA886119F5230e5a2C25d", "blockNumber": 33563508, - "paramsSchema": "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] gates,uint256 minOwned", + "configureProductSchema": "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] gates,uint256 minOwned", "transactionHash": "0x9dd101a6155b432849cb33f65c5d4b9f6875e6b1e7bf806005a8a6b0d070f634" } ] }, "pricing": { - "NFTDiscount": [ - { - "address": "0xb830a457d2f51d4cA1136b97FB30DF6366CFe2f5", - "blockNumber": 33596708, - "paramsSchema": "(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[] discounts", - "transactionHash": "0x966be5fa1da3fab7c5027c9acb3f118307997e30687188b4745d74fd26ad9e7e" - } - ], "LinearVRGDAPrices": [ { "address": "0xEC68E30182F4298b7032400B7ce809da613e4449", "blockNumber": 33511188, - "paramsSchema": "int184 priceDecayPercent,uint16 min,int256 perTimeUnit", + "configureProductSchema": "int184 priceDecayPercent,uint16 min,int256 perTimeUnit", "transactionHash": "0xf2667ce20d07561e59c8d3e3de135bbd895c5b08bb57fe487c6c8197c91a0d73" } ], @@ -62,18 +54,26 @@ { "address": "0x2b02cC8528EF18abf8185543CEC29A94F0542c8F", "blockNumber": 33511209, - "paramsSchema": "int184 priceDecayPercent,uint16 min,int256 timescale", + "configureProductSchema": "int184 priceDecayPercent,uint16 min,int256 timescale", "transactionHash": "0x2b27380661a38b54dbccf634305622296034ddfccf5865a07fbfb7810ea41025" } + ], + "NFTDiscount": [ + { + "address": "0xba7D3668EeE9324f406243032B2786bA7Edb0a35", + "blockNumber": 36415352, + "configureProductSchema": "(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[] discounts", + "transactionHash": "0x14176f01e39dae7eeab1ec3704085ff102ff355df34a2e631888f4f2b47c2b92" + } ] }, "pricingActions": { "FirstForFree": [ { - "address": "0x2C18D37b8229233F672bF406bCe8799BCfD43B5A", - "blockNumber": 33510960, - "paramsSchema": "(address tokenAddress,uint8 tokenType,uint80 tokenId,uint8 minQuantity)[] eligibleTokens,address mintToken,uint88 mintTokenId,uint8 freeUnits", - "transactionHash": "0x8bd0b9de8edd704899210f8450096adf7f4f853030a7ba0a2f64494bc1ba726e" + "address": "0xecC477959f1901a4B5f5C9Ab2B694F701F74819c", + "blockNumber": 36415374, + "configureProductSchema": "(address tokenAddress,uint8 tokenType,uint80 tokenId,uint8 minQuantity)[] eligibleTokens,address mintToken,uint88 mintTokenId,uint8 freeUnits", + "transactionHash": "0xfd91db10b4e70513a7d2ac682ac8a4095f713f4971d41929e8581bdacc90b340" } ] } diff --git a/script/Seed.s.sol b/script/Seed.s.sol index 8ba86be..fb3b45f 100644 --- a/script/Seed.s.sol +++ b/script/Seed.s.sol @@ -23,41 +23,42 @@ contract SeedHooksScript is Script { function run() external { vm.startBroadcast(); + string memory json = vm.readFile("./deployments/addresses.json"); Hook[] memory hooks = new Hook[](9); hooks[0] = Hook({ - hookAddress: 0x157428DD791E03c20880D22C3dA2B66A36B5cF26, + hookAddress: vm.parseJsonAddress(json, ".actions.Allowlisted.[0].address"), code: address(new Allowlisted(Contracts.PRODUCTS_MODULE)).code }); hooks[1] = Hook({ - hookAddress: 0x26A1C86B555013995Fc72864D261fDe984752E7c, + hookAddress: vm.parseJsonAddress(json, ".actions.ERC20Gated.[0].address"), code: address(new ERC20Gated(Contracts.PRODUCTS_MODULE)).code }); hooks[2] = Hook({ - hookAddress: 0x67f9799FaC1D53C63217BEE47f553150F5BB0836, + hookAddress: vm.parseJsonAddress(json, ".actions.ERC20Mint.[0].address"), code: address(new ERC20Mint(Contracts.PRODUCTS_MODULE)).code }); hooks[3] = Hook({ - hookAddress: 0x2b6488115FAa50142E140172CbCd60e6370675F7, + hookAddress: vm.parseJsonAddress(json, ".actions.ERC721Mint.[0].address"), code: address(new ERC721Mint(Contracts.PRODUCTS_MODULE)).code }); hooks[4] = Hook({ - hookAddress: 0xD4eF7A46bF4c58036eaCA886119F5230e5a2C25d, + hookAddress: vm.parseJsonAddress(json, ".actions.NFTGated.[0].address"), code: address(new NFTGated(Contracts.PRODUCTS_MODULE)).code }); hooks[5] = Hook({ - hookAddress: 0xb830a457d2f51d4cA1136b97FB30DF6366CFe2f5, + hookAddress: vm.parseJsonAddress(json, ".pricing.NFTDiscount.[0].address"), code: address(new NFTDiscount(Contracts.PRODUCTS_MODULE)).code }); hooks[6] = Hook({ - hookAddress: 0xEC68E30182F4298b7032400B7ce809da613e4449, + hookAddress: vm.parseJsonAddress(json, ".pricing.LinearVRGDAPrices.[0].address"), code: address(new LinearVRGDAPrices(Contracts.PRODUCTS_MODULE)).code }); hooks[7] = Hook({ - hookAddress: 0x2b02cC8528EF18abf8185543CEC29A94F0542c8F, + hookAddress: vm.parseJsonAddress(json, ".pricing.LogisticVRGDAPrices.[0].address"), code: address(new LogisticVRGDAPrices(Contracts.PRODUCTS_MODULE)).code }); hooks[8] = Hook({ - hookAddress: 0x2C18D37b8229233F672bF406bCe8799BCfD43B5A, + hookAddress: vm.parseJsonAddress(json, ".pricingActions.FirstForFree.[0].address"), code: address(new FirstForFree(Contracts.PRODUCTS_MODULE)).code }); 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/src/hooks/actions/Allowlisted/Allowlisted.sol b/src/hooks/actions/Allowlisted/Allowlisted.sol index eb722e3..ce76f0d 100644 --- a/src/hooks/actions/Allowlisted/Allowlisted.sol +++ b/src/hooks/actions/Allowlisted/Allowlisted.sol @@ -3,9 +3,9 @@ pragma solidity ^0.8.30; import {MerkleProof} from "@openzeppelin-4.8.0/utils/cryptography/MerkleProof.sol"; import { - IProductsModule, RegistryProductAction, ProductHookRegistry, + IProductsModule, IProductAction, IProductHookRegistry } from "@/utils/RegistryProductAction.sol"; diff --git a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol index 6ad8410..fd2abbb 100644 --- a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol @@ -3,14 +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, - ProductHookRegistry, - IProductHookRegistry, - IProductTypeHookRegistry, - ISlicerHookRegistry, - IProductsModule -} from "@/utils/RegistryPrice.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"; diff --git a/test/utils/ProductHookRegistryTest.sol b/test/utils/ProductHookRegistryTest.sol index ec3340b..b30a5c1 100644 --- a/test/utils/ProductHookRegistryTest.sol +++ b/test/utils/ProductHookRegistryTest.sol @@ -9,7 +9,7 @@ import {SliceContext} from "@/utils/RegistryProductAction.sol"; bytes32 constant PRODUCT_MANAGER_ROLE = bytes32(uint256(0x04)); abstract contract ProductHookRegistryTest is HookTest { - function testParamsSchema() public view { + function testConfigureProductSchema() public view { string memory schema = IProductHookRegistry(hook).configureProductSchema(); assertTrue(bytes(schema).length > 0); } From 724363272c68b41ec1a77529963819c40fe972a2 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 5 Oct 2025 14:52:04 +0200 Subject: [PATCH 20/30] NFTDiscount tests --- test/pricing/TieredDiscount/NFTDiscount.t.sol | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) diff --git a/test/pricing/TieredDiscount/NFTDiscount.t.sol b/test/pricing/TieredDiscount/NFTDiscount.t.sol index e172fd3..ee573d6 100644 --- a/test/pricing/TieredDiscount/NFTDiscount.t.sol +++ b/test/pricing/TieredDiscount/NFTDiscount.t.sol @@ -4,10 +4,12 @@ pragma solidity ^0.8.30; import {RegistryProductPriceTest} from "@test/utils/RegistryProductPriceTest.sol"; import { IProductsModule, + HookRegistry, NFTDiscount, DiscountParams, NFTType } from "@/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol"; +import {SliceContext} from "slice/utils/SliceContext.sol"; import {MockERC721} from "@test/utils/mocks/MockERC721.sol"; import {MockERC1155} from "@test/utils/mocks/MockERC1155.sol"; @@ -15,6 +17,7 @@ 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% @@ -35,6 +38,229 @@ 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 testProductTypePriceSchema() public view { + string memory schema = erc721GatedDiscount.productTypePriceSchema(); + + assertEq(schema, erc721GatedDiscount.productPriceSchema()); + } + + 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); + + uint256 price = + erc721GatedDiscount.slicerProductPrice(slicerId, ETH, basePrice, quantity, buyer, ""); + + assertEq(price, (basePrice - (basePrice * percentDiscountOne) / 1e4)); + + nftTwo.mint(buyer); + + uint256 priceWithHigherDiscount = + erc721GatedDiscount.slicerProductPrice(slicerId, ETH, basePrice, quantity, buyer, ""); + + assertEq(priceWithHigherDiscount, (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 testSlicerProductPriceSchema() public view { + string memory schema = erc721GatedDiscount.slicerProductPriceSchema(); + + assertEq(schema, erc721GatedDiscount.productPriceSchema()); + } + + 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); + uint256 price = erc721GatedDiscount.slicerProductPrice(slicerId, ETH, basePrice, quantity, buyer3, ""); + + assertEq(price, 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); + + uint256 price = erc721GatedDiscount.slicerProductPrice(slicerId, ETH, basePrice, quantity, buyer, ""); + + assertEq(price, (basePrice - (basePrice * percentDiscountOne) / 1e4)); + + vm.prank(productOwner); + erc721GatedDiscount.configureSlicer(slicerId, abi.encode(new DiscountParams[](0))); + + uint256 resetPrice = erc721GatedDiscount.slicerProductPrice(slicerId, ETH, basePrice, quantity, buyer, ""); + + assertEq(resetPrice, basePrice); + } + function testConfigureProduct__ETH() public { DiscountParams[] memory discountParams = new DiscountParams[](1); From 8f86ed28f232f02d930101312a27bd2ad90cd587 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 5 Oct 2025 15:56:43 +0200 Subject: [PATCH 21/30] update WriteAddresses logic --- deployments/addresses.json | 108 ++++++++--- script/ScriptUtils.sol | 173 +++++++++++++++--- test/actions/Allowlisted/Allowlisted.t.sol | 4 +- test/pricing/TieredDiscount/NFTDiscount.t.sol | 17 +- 4 files changed, 233 insertions(+), 69 deletions(-) diff --git a/deployments/addresses.json b/deployments/addresses.json index abcdb84..8dfbe7b 100644 --- a/deployments/addresses.json +++ b/deployments/addresses.json @@ -2,78 +2,132 @@ "actions": { "Allowlisted": [ { - "address": "0x57c6Ae75B3903755ADb1E1140d80b91FE12394D6", - "blockNumber": 36415274, + "address": "0x87f726ac5D1023919840bebde2Da2efb5Da816ad", + "blockNumber": 36441333, "configureProductSchema": "bytes32 merkleRoot", - "transactionHash": "0xe1c603de0844e496a13bfb4996f6436b198a788aedfd384d227b1f85b09c6b4a" + "configureProductTypeSchema": "not-supported", + "configureSlicerSchema": "not-supported", + "onProductPurchaseSchema": "not-supported", + "productPriceSchema": "not-supported", + "productTypePriceSchema": "not-supported", + "slicerProductPriceSchema": "not-supported", + "transactionHash": "0x57419dee52082559db1d966b1b139bb79ac59c4533de43153dd11fda30b765c1" } ], "ERC20Gated": [ { - "address": "0x26A1C86B555013995Fc72864D261fDe984752E7c", - "blockNumber": 33558792, + "address": "0x247ebf608A0028DE933f1F90eE3920864D3fC621", + "blockNumber": 36440973, "configureProductSchema": "(address erc20,uint256 amount)[] erc20Gates", - "transactionHash": "0x3a7c01ede05a34280073479d5cdf1f35e41d9f08c36a71f358b9c503ccc54526" + "configureProductTypeSchema": "not-supported", + "configureSlicerSchema": "not-supported", + "onProductPurchaseSchema": "not-supported", + "productPriceSchema": "not-supported", + "productTypePriceSchema": "not-supported", + "slicerProductPriceSchema": "not-supported", + "transactionHash": "0xac40db0f5c63ec520e671db10b018018900a332d563c99bc9828a6cc20b48863" } ], "ERC20Mint": [ { - "address": "0x67f9799FaC1D53C63217BEE47f553150F5BB0836", - "blockNumber": 33520592, + "address": "0x8B03B4dE7F6FD7A7BDD60Cb00aa49E144e45cdd0", + "blockNumber": 36441450, "configureProductSchema": "string name,string symbol,uint256 premintAmount,address premintReceiver,bool revertOnMaxSupplyReached,uint256 maxSupply,uint256 tokensPerUnit", - "transactionHash": "0xfcd7e8fe47aa509afa0acdef86b741656c2602626f3258f8a83b359c665d6b04" + "configureProductTypeSchema": "not-supported", + "configureSlicerSchema": "not-supported", + "onProductPurchaseSchema": "not-supported", + "productPriceSchema": "not-supported", + "productTypePriceSchema": "not-supported", + "slicerProductPriceSchema": "not-supported", + "transactionHash": "0xfd77332c2aba9e05a71419b87ddb0a136f85675f7d0f13cf7c46c3e65c99f44c" } ], "ERC721Mint": [ { - "address": "0x2b6488115FAa50142E140172CbCd60e6370675F7", - "blockNumber": 33511082, + "address": "0x48eAf12a1dC2a6dfACcb4C03F381B07ad5D5961C", + "blockNumber": 36441490, "configureProductSchema": "string name,string symbol,address royaltyReceiver,uint256 royaltyFraction,string baseURI,string tokenURI,bool revertOnMaxSupplyReached,uint256 maxSupply", - "transactionHash": "0x4981b3b67c0b8abe6a942b3ca86643f2b1dfdbcce59d6c819ad20a82814956e0" + "configureProductTypeSchema": "not-supported", + "configureSlicerSchema": "not-supported", + "onProductPurchaseSchema": "not-supported", + "productPriceSchema": "not-supported", + "productTypePriceSchema": "not-supported", + "slicerProductPriceSchema": "not-supported", + "transactionHash": "0x4851d2daedfa5de5a6a60a44fb9709e8c865039660c373d6d96f06c4ec2369f5" } ], "NFTGated": [ { - "address": "0xD4eF7A46bF4c58036eaCA886119F5230e5a2C25d", - "blockNumber": 33563508, + "address": "0x560463f7bF3DFDB4Cff4f96c3399313d610Dd349", + "blockNumber": 36441510, "configureProductSchema": "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] gates,uint256 minOwned", - "transactionHash": "0x9dd101a6155b432849cb33f65c5d4b9f6875e6b1e7bf806005a8a6b0d070f634" + "configureProductTypeSchema": "not-supported", + "configureSlicerSchema": "not-supported", + "onProductPurchaseSchema": "", + "productPriceSchema": "not-supported", + "productTypePriceSchema": "not-supported", + "slicerProductPriceSchema": "not-supported", + "transactionHash": "0x3bef5e07d303ab1028a7928f9ef1ffb1613b957aff4bfe481ae79a6fd1029e2d" } ] }, "pricing": { "LinearVRGDAPrices": [ { - "address": "0xEC68E30182F4298b7032400B7ce809da613e4449", - "blockNumber": 33511188, + "address": "0x67AEdfe386a078DF350D4b1E033AB735c1fCD0C9", + "blockNumber": 36441538, "configureProductSchema": "int184 priceDecayPercent,uint16 min,int256 perTimeUnit", - "transactionHash": "0xf2667ce20d07561e59c8d3e3de135bbd895c5b08bb57fe487c6c8197c91a0d73" + "configureProductTypeSchema": "not-supported", + "configureSlicerSchema": "not-supported", + "onProductPurchaseSchema": "not-supported", + "productPriceSchema": "not-supported", + "productTypePriceSchema": "not-supported", + "slicerProductPriceSchema": "not-supported", + "transactionHash": "0xd5ad41a2f994e0151edaf566e8114b703fa1843c4bbd4a8ed79b4312e22a4843" } ], "LogisticVRGDAPrices": [ { - "address": "0x2b02cC8528EF18abf8185543CEC29A94F0542c8F", - "blockNumber": 33511209, + "address": "0x12b56C70fd76D45b578C50aD2eDB582b44d75cC1", + "blockNumber": 36441564, "configureProductSchema": "int184 priceDecayPercent,uint16 min,int256 timescale", - "transactionHash": "0x2b27380661a38b54dbccf634305622296034ddfccf5865a07fbfb7810ea41025" + "configureProductTypeSchema": "not-supported", + "configureSlicerSchema": "not-supported", + "onProductPurchaseSchema": "not-supported", + "productPriceSchema": "", + "productTypePriceSchema": "not-supported", + "slicerProductPriceSchema": "not-supported", + "transactionHash": "0x72fbbdd585b2fd5a2c5be0431e14c0ddee9a266f0c2c6d259dfc9b76a938122a" } ], "NFTDiscount": [ { - "address": "0xba7D3668EeE9324f406243032B2786bA7Edb0a35", - "blockNumber": 36415352, + "address": "0xD92820f4836b23DCe9f7696b12a5C877ec667128", + "blockNumber": 36441359, "configureProductSchema": "(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[] discounts", - "transactionHash": "0x14176f01e39dae7eeab1ec3704085ff102ff355df34a2e631888f4f2b47c2b92" + "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": "not-supported", + "productTypePriceSchema": "not-supported", + "slicerProductPriceSchema": "", + "transactionHash": "0xaf7bb92631cfa26e513b23bd48d60535244a9161e497d25d55ba139890414e3c" } ] }, "pricingActions": { "FirstForFree": [ { - "address": "0xecC477959f1901a4B5f5C9Ab2B694F701F74819c", - "blockNumber": 36415374, + "address": "0x718308153116e90393804ae7a58a8e7d5BDb6482", + "blockNumber": 36441582, "configureProductSchema": "(address tokenAddress,uint8 tokenType,uint80 tokenId,uint8 minQuantity)[] eligibleTokens,address mintToken,uint88 mintTokenId,uint8 freeUnits", - "transactionHash": "0xfd91db10b4e70513a7d2ac682ac8a4095f713f4971d41929e8581bdacc90b340" + "configureProductTypeSchema": "not-supported", + "configureSlicerSchema": "not-supported", + "onProductPurchaseSchema": "", + "productPriceSchema": "", + "productTypePriceSchema": "not-supported", + "slicerProductPriceSchema": "not-supported", + "transactionHash": "0x59033c01e7228c3f28d25ce233bb729e595515923078498e726d052f038a87aa" } ] } diff --git a/script/ScriptUtils.sol b/script/ScriptUtils.sol index 67984b4..8658f0a 100644 --- a/script/ScriptUtils.sol +++ b/script/ScriptUtils.sol @@ -8,6 +8,14 @@ import {ISliceCore} from "slice/interfaces/ISliceCore.sol"; import {IProductsModule} from "slice/interfaces/IProductsModule.sol"; import {IFundsModule} from "slice/interfaces/IFundsModule.sol"; import {IProductHookRegistry} from "slice/interfaces/hooks/IHookRegistry/IProductHookRegistry.sol"; +import { + IProductTypeHookRegistry, + ISlicerHookRegistry, + IProductPrice, + IProductTypePrice, + ISlicerProductPrice, + IProductAction +} from "slice/interfaces.sol"; /** * Helper contract to enforce correct chain selection in scripts @@ -87,6 +95,12 @@ abstract contract SetUpContractsList is Script { uint256 blockNumber; // 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 productPriceSchema; + string productTypePriceSchema; + string slicerProductPriceSchema; + string onProductPurchaseSchema; bytes32 transactionHash; } @@ -135,11 +149,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, "configureProductSchema", existingData[j].configureProductSchema - ); + _setFields(idx, existingData[j]); arrStrings[j] = vm.serializeBytes32(idx, "transactionHash", existingData[j].transactionHash); } @@ -201,6 +211,134 @@ 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); + + vm.serializeString( + idx, + "productPriceSchema", + _isProductPrice(existingData.contractAddress) ? existingData.productPriceSchema : "not-supported" + ); + vm.serializeString( + idx, + "onProductPurchaseSchema", + _isProductAction(existingData.contractAddress) ? existingData.onProductPurchaseSchema : "not-supported" + ); + vm.serializeString( + idx, + "configureProductTypeSchema", + _isProductTypeHookRegistry(existingData.contractAddress) + ? existingData.configureProductTypeSchema + : "not-supported" + ); + vm.serializeString( + idx, + "configureSlicerSchema", + _isSlicerHookRegistry(existingData.contractAddress) ? existingData.configureSlicerSchema : "not-supported" + ); + vm.serializeString( + idx, + "productTypePriceSchema", + _isProductTypePrice(existingData.contractAddress) ? existingData.productTypePriceSchema : "not-supported" + ); + vm.serializeString( + idx, + "slicerProductPriceSchema", + _isSlicerProductPrice(existingData.contractAddress) + ? existingData.slicerProductPriceSchema + : "not-supported" + ); + } + + 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", + "productPriceSchema", + _isProductPrice(transaction.contractAddress) + ? IProductPrice(transaction.contractAddress).productPriceSchema() + : "not-supported" + ); + + vm.serializeString( + "0", + "onProductPurchaseSchema", + _isProductAction(transaction.contractAddress) + ? IProductAction(transaction.contractAddress).onProductPurchaseSchema() + : "not-supported" + ); + + 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", + "productTypePriceSchema", + _isProductTypePrice(transaction.contractAddress) + ? IProductTypePrice(transaction.contractAddress).productTypePriceSchema() + : "not-supported" + ); + + vm.serializeString( + "0", + "slicerProductPriceSchema", + _isSlicerProductPrice(transaction.contractAddress) + ? ISlicerProductPrice(transaction.contractAddress).slicerProductPriceSchema() + : "not-supported" + ); + + json[0] = vm.serializeBytes32("0", "transactionHash", transaction.hash); + + return json; + } + function _buildJsonArray( string memory existingAddresses, string memory key, @@ -213,36 +351,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", - "configureProductSchema", - IProductHookRegistry(transaction.contractAddress).configureProductSchema() - ); - // TODO: Add other params to the json? such as configure[ProductType/Slicer]Schema and [product/productType/slicer]PriceSchema - 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, "configureProductSchema", existingContractAddress.configureProductSchema); + _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", - "configureProductSchema", - IProductHookRegistry(transaction.contractAddress).configureProductSchema() - ); - json[0] = vm.serializeBytes32("0", "transactionHash", transaction.hash); + json = _serializeObject(new string[](1), transaction, receipt); } } diff --git a/test/actions/Allowlisted/Allowlisted.t.sol b/test/actions/Allowlisted/Allowlisted.t.sol index 9418a75..c95edab 100644 --- a/test/actions/Allowlisted/Allowlisted.t.sol +++ b/test/actions/Allowlisted/Allowlisted.t.sol @@ -77,7 +77,9 @@ contract AllowlistedTest is RegistryProductActionTest { 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))); + 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) { diff --git a/test/pricing/TieredDiscount/NFTDiscount.t.sol b/test/pricing/TieredDiscount/NFTDiscount.t.sol index ee573d6..7d1dd8d 100644 --- a/test/pricing/TieredDiscount/NFTDiscount.t.sol +++ b/test/pricing/TieredDiscount/NFTDiscount.t.sol @@ -80,12 +80,7 @@ contract NFTDiscountTest is RegistryProductPriceTest { } function testConfigureProductType_AccessControl() public { - vm.expectRevert( - abi.encodeWithSelector( - SliceContext.NotAuthorized.selector, - bytes32(uint256(1 << 1 | 1 << 3)) - ) - ); + vm.expectRevert(abi.encodeWithSelector(SliceContext.NotAuthorized.selector, bytes32(uint256(1 << 1 | 1 << 3)))); erc721GatedDiscount.configureProductType(slicerId, productTypeId, ""); } @@ -179,8 +174,7 @@ contract NFTDiscountTest is RegistryProductPriceTest { uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - uint256 price = - erc721GatedDiscount.slicerProductPrice(slicerId, ETH, basePrice, quantity, buyer, ""); + uint256 price = erc721GatedDiscount.slicerProductPrice(slicerId, ETH, basePrice, quantity, buyer, ""); assertEq(price, (basePrice - (basePrice * percentDiscountOne) / 1e4)); @@ -193,12 +187,7 @@ contract NFTDiscountTest is RegistryProductPriceTest { } function testConfigureSlicer_AccessControl() public { - vm.expectRevert( - abi.encodeWithSelector( - SliceContext.NotAuthorized.selector, - bytes32(uint256(1 << 1 | 1 << 3)) - ) - ); + vm.expectRevert(abi.encodeWithSelector(SliceContext.NotAuthorized.selector, bytes32(uint256(1 << 1 | 1 << 3)))); erc721GatedDiscount.configureSlicer(slicerId, ""); } From 7a586f33462a5ac5e2297b3bda429ae9794df54e Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 5 Oct 2025 17:58:29 +0200 Subject: [PATCH 22/30] fix: ScriptUtils --- deployments/addresses.json | 14 ++++---- script/ScriptUtils.sol | 67 ++++++++++++-------------------------- 2 files changed, 27 insertions(+), 54 deletions(-) diff --git a/deployments/addresses.json b/deployments/addresses.json index 8dfbe7b..03b7c8f 100644 --- a/deployments/addresses.json +++ b/deployments/addresses.json @@ -7,7 +7,7 @@ "configureProductSchema": "bytes32 merkleRoot", "configureProductTypeSchema": "not-supported", "configureSlicerSchema": "not-supported", - "onProductPurchaseSchema": "not-supported", + "onProductPurchaseSchema": "", "productPriceSchema": "not-supported", "productTypePriceSchema": "not-supported", "slicerProductPriceSchema": "not-supported", @@ -21,7 +21,7 @@ "configureProductSchema": "(address erc20,uint256 amount)[] erc20Gates", "configureProductTypeSchema": "not-supported", "configureSlicerSchema": "not-supported", - "onProductPurchaseSchema": "not-supported", + "onProductPurchaseSchema": "", "productPriceSchema": "not-supported", "productTypePriceSchema": "not-supported", "slicerProductPriceSchema": "not-supported", @@ -35,7 +35,7 @@ "configureProductSchema": "string name,string symbol,uint256 premintAmount,address premintReceiver,bool revertOnMaxSupplyReached,uint256 maxSupply,uint256 tokensPerUnit", "configureProductTypeSchema": "not-supported", "configureSlicerSchema": "not-supported", - "onProductPurchaseSchema": "not-supported", + "onProductPurchaseSchema": "", "productPriceSchema": "not-supported", "productTypePriceSchema": "not-supported", "slicerProductPriceSchema": "not-supported", @@ -49,7 +49,7 @@ "configureProductSchema": "string name,string symbol,address royaltyReceiver,uint256 royaltyFraction,string baseURI,string tokenURI,bool revertOnMaxSupplyReached,uint256 maxSupply", "configureProductTypeSchema": "not-supported", "configureSlicerSchema": "not-supported", - "onProductPurchaseSchema": "not-supported", + "onProductPurchaseSchema": "", "productPriceSchema": "not-supported", "productTypePriceSchema": "not-supported", "slicerProductPriceSchema": "not-supported", @@ -80,7 +80,7 @@ "configureProductTypeSchema": "not-supported", "configureSlicerSchema": "not-supported", "onProductPurchaseSchema": "not-supported", - "productPriceSchema": "not-supported", + "productPriceSchema": "", "productTypePriceSchema": "not-supported", "slicerProductPriceSchema": "not-supported", "transactionHash": "0xd5ad41a2f994e0151edaf566e8114b703fa1843c4bbd4a8ed79b4312e22a4843" @@ -108,8 +108,8 @@ "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": "not-supported", - "productTypePriceSchema": "not-supported", + "productPriceSchema": "", + "productTypePriceSchema": "", "slicerProductPriceSchema": "", "transactionHash": "0xaf7bb92631cfa26e513b23bd48d60535244a9161e497d25d55ba139890414e3c" } diff --git a/script/ScriptUtils.sol b/script/ScriptUtils.sol index 8658f0a..44f8adf 100644 --- a/script/ScriptUtils.sol +++ b/script/ScriptUtils.sol @@ -97,10 +97,10 @@ abstract contract SetUpContractsList is Script { string configureProductSchema; string configureProductTypeSchema; string configureSlicerSchema; + string onProductPurchaseSchema; string productPriceSchema; string productTypePriceSchema; string slicerProductPriceSchema; - string onProductPurchaseSchema; bytes32 transactionHash; } @@ -216,40 +216,13 @@ abstract contract SetUpContractsList is Script { vm.serializeUint(idx, "blockNumber", existingData.blockNumber); vm.serializeString(idx, "configureProductSchema", existingData.configureProductSchema); - vm.serializeString( - idx, - "productPriceSchema", - _isProductPrice(existingData.contractAddress) ? existingData.productPriceSchema : "not-supported" - ); - vm.serializeString( - idx, - "onProductPurchaseSchema", - _isProductAction(existingData.contractAddress) ? existingData.onProductPurchaseSchema : "not-supported" - ); - vm.serializeString( - idx, - "configureProductTypeSchema", - _isProductTypeHookRegistry(existingData.contractAddress) - ? existingData.configureProductTypeSchema - : "not-supported" - ); - vm.serializeString( - idx, - "configureSlicerSchema", - _isSlicerHookRegistry(existingData.contractAddress) ? existingData.configureSlicerSchema : "not-supported" - ); - vm.serializeString( - idx, - "productTypePriceSchema", - _isProductTypePrice(existingData.contractAddress) ? existingData.productTypePriceSchema : "not-supported" - ); - vm.serializeString( - idx, - "slicerProductPriceSchema", - _isSlicerProductPrice(existingData.contractAddress) - ? existingData.slicerProductPriceSchema - : "not-supported" - ); + // 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); + vm.serializeString(idx, "productTypePriceSchema", existingData.productTypePriceSchema); + vm.serializeString(idx, "slicerProductPriceSchema", existingData.slicerProductPriceSchema); } function _isProductTypeHookRegistry(address contractAddress) internal view returns (bool) { @@ -288,33 +261,33 @@ abstract contract SetUpContractsList is Script { vm.serializeString( "0", - "productPriceSchema", - _isProductPrice(transaction.contractAddress) - ? IProductPrice(transaction.contractAddress).productPriceSchema() + "configureProductTypeSchema", + _isProductTypeHookRegistry(transaction.contractAddress) + ? IProductTypeHookRegistry(transaction.contractAddress).configureProductTypeSchema() : "not-supported" ); vm.serializeString( "0", - "onProductPurchaseSchema", - _isProductAction(transaction.contractAddress) - ? IProductAction(transaction.contractAddress).onProductPurchaseSchema() + "configureSlicerSchema", + _isSlicerHookRegistry(transaction.contractAddress) + ? ISlicerHookRegistry(transaction.contractAddress).configureSlicerSchema() : "not-supported" ); vm.serializeString( "0", - "configureProductTypeSchema", - _isProductTypeHookRegistry(transaction.contractAddress) - ? IProductTypeHookRegistry(transaction.contractAddress).configureProductTypeSchema() + "onProductPurchaseSchema", + _isProductAction(transaction.contractAddress) + ? IProductAction(transaction.contractAddress).onProductPurchaseSchema() : "not-supported" ); vm.serializeString( "0", - "configureSlicerSchema", - _isSlicerHookRegistry(transaction.contractAddress) - ? ISlicerHookRegistry(transaction.contractAddress).configureSlicerSchema() + "productPriceSchema", + _isProductPrice(transaction.contractAddress) + ? IProductPrice(transaction.contractAddress).productPriceSchema() : "not-supported" ); From b345c2e3779864310a3e1e754e9c4c7df2e48e72 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 5 Oct 2025 19:48:58 +0200 Subject: [PATCH 23/30] indexer: add support for additional hooks --- src/hooks/pricing/README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/hooks/pricing/README.md b/src/hooks/pricing/README.md index a820cdc..acea57c 100644 --- a/src/hooks/pricing/README.md +++ b/src/hooks/pricing/README.md @@ -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 `configureProductSchema()` +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 From 04c1fe9de17f80e255f0114ac45f94f6ead93e61 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 29 Oct 2025 02:22:26 +0100 Subject: [PATCH 24/30] slicer pricing strategies final --- .../pricing/TieredDiscount/TieredDiscount.sol | 31 +++++++---- test/pricing/TieredDiscount/NFTDiscount.t.sol | 53 ++++++++++++++----- 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol index d26c337..505d30a 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 {RegistryPrice, IProductPrice, IProductTypePrice, ISlicerProductPrice} from "@/utils/RegistryPrice.sol"; +import { + RegistryPrice, + IProductPrice, + IProductTypePrice, + ISlicerProductPrice, + ProductLineItem +} from "@/utils/RegistryPrice.sol"; import {DiscountParams} from "./types/DiscountParams.sol"; /** @@ -77,15 +83,22 @@ abstract contract TieredDiscount is RegistryPrice { /** * @inheritdoc ISlicerProductPrice */ - function slicerProductPrice(uint256 slicerId, address, uint256 basePrice, uint256, address buyer, bytes memory) - public - view - override - returns (uint256) - { + 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]; - - return _productPrice(buyer, basePrice, discountParams); + updatedBasePrices = new uint256[](basePrices.length); + + for (uint256 i; i < productLineItems.length;) { + updatedBasePrices[i] = _productPrice(buyer, basePrices[i], discountParams); + unchecked { + ++i; + } + } } /*////////////////////////////////////////////////////////////// diff --git a/test/pricing/TieredDiscount/NFTDiscount.t.sol b/test/pricing/TieredDiscount/NFTDiscount.t.sol index 7d1dd8d..d68eea7 100644 --- a/test/pricing/TieredDiscount/NFTDiscount.t.sol +++ b/test/pricing/TieredDiscount/NFTDiscount.t.sol @@ -9,6 +9,9 @@ import { 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"; @@ -174,16 +177,24 @@ contract NFTDiscountTest is RegistryProductPriceTest { uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - uint256 price = erc721GatedDiscount.slicerProductPrice(slicerId, ETH, basePrice, quantity, buyer, ""); + 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(price, (basePrice - (basePrice * percentDiscountOne) / 1e4)); + assertEq(newBasePrices[0], (basePrice - (basePrice * percentDiscountOne) / 1e4)); nftTwo.mint(buyer); - uint256 priceWithHigherDiscount = - erc721GatedDiscount.slicerProductPrice(slicerId, ETH, basePrice, quantity, buyer, ""); + newBasePrices = + erc721GatedDiscount.slicerProductPrice(slicerId, ETH,buyer, productLineItems, basePrices); - assertEq(priceWithHigherDiscount, (basePrice - (basePrice * percentDiscountTwo) / 1e4)); + assertEq(newBasePrices[0], (basePrice - (basePrice * percentDiscountTwo) / 1e4)); } function testConfigureSlicer_AccessControl() public { @@ -210,9 +221,19 @@ contract NFTDiscountTest is RegistryProductPriceTest { erc721GatedDiscount.configureSlicer(slicerId, abi.encode(new DiscountParams[](0))); uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - uint256 price = erc721GatedDiscount.slicerProductPrice(slicerId, ETH, basePrice, quantity, buyer3, ""); - assertEq(price, basePrice); + + 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 { @@ -238,16 +259,24 @@ contract NFTDiscountTest is RegistryProductPriceTest { uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - uint256 price = erc721GatedDiscount.slicerProductPrice(slicerId, ETH, basePrice, quantity, buyer, ""); + 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(price, (basePrice - (basePrice * percentDiscountOne) / 1e4)); + assertEq(newBasePrices[0], (basePrice - (basePrice * percentDiscountOne) / 1e4)); vm.prank(productOwner); erc721GatedDiscount.configureSlicer(slicerId, abi.encode(new DiscountParams[](0))); - uint256 resetPrice = erc721GatedDiscount.slicerProductPrice(slicerId, ETH, basePrice, quantity, buyer, ""); - - assertEq(resetPrice, basePrice); + newBasePrices = erc721GatedDiscount.slicerProductPrice(slicerId, ETH,buyer, productLineItems, basePrices); + + assertEq(newBasePrices[0], basePrice); } function testConfigureProduct__ETH() public { From 6cf8a423bf28d41beb9fcdd2e973c78b125d1a3a Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 29 Oct 2025 03:23:13 +0100 Subject: [PATCH 25/30] product type actions init + remove data from prodtype and slicer hooks --- script/ScriptUtils.sol | 20 ------- test/pricing/TieredDiscount/NFTDiscount.t.sol | 54 ++++++------------- 2 files changed, 16 insertions(+), 58 deletions(-) diff --git a/script/ScriptUtils.sol b/script/ScriptUtils.sol index 44f8adf..7637102 100644 --- a/script/ScriptUtils.sol +++ b/script/ScriptUtils.sol @@ -99,8 +99,6 @@ abstract contract SetUpContractsList is Script { string configureSlicerSchema; string onProductPurchaseSchema; string productPriceSchema; - string productTypePriceSchema; - string slicerProductPriceSchema; bytes32 transactionHash; } @@ -221,8 +219,6 @@ abstract contract SetUpContractsList is Script { vm.serializeString(idx, "configureSlicerSchema", existingData.configureSlicerSchema); vm.serializeString(idx, "onProductPurchaseSchema", existingData.onProductPurchaseSchema); vm.serializeString(idx, "productPriceSchema", existingData.productPriceSchema); - vm.serializeString(idx, "productTypePriceSchema", existingData.productTypePriceSchema); - vm.serializeString(idx, "slicerProductPriceSchema", existingData.slicerProductPriceSchema); } function _isProductTypeHookRegistry(address contractAddress) internal view returns (bool) { @@ -291,22 +287,6 @@ abstract contract SetUpContractsList is Script { : "not-supported" ); - vm.serializeString( - "0", - "productTypePriceSchema", - _isProductTypePrice(transaction.contractAddress) - ? IProductTypePrice(transaction.contractAddress).productTypePriceSchema() - : "not-supported" - ); - - vm.serializeString( - "0", - "slicerProductPriceSchema", - _isSlicerProductPrice(transaction.contractAddress) - ? ISlicerProductPrice(transaction.contractAddress).slicerProductPriceSchema() - : "not-supported" - ); - json[0] = vm.serializeBytes32("0", "transactionHash", transaction.hash); return json; diff --git a/test/pricing/TieredDiscount/NFTDiscount.t.sol b/test/pricing/TieredDiscount/NFTDiscount.t.sol index d68eea7..088d25c 100644 --- a/test/pricing/TieredDiscount/NFTDiscount.t.sol +++ b/test/pricing/TieredDiscount/NFTDiscount.t.sol @@ -9,9 +9,7 @@ import { DiscountParams, NFTType } from "@/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol"; -import { - ProductLineItem -} from "@/hooks/pricing/TieredDiscount/TieredDiscount.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"; @@ -95,12 +93,6 @@ contract NFTDiscountTest is RegistryProductPriceTest { assertTrue(bytes(schema).length > 0); } - function testProductTypePriceSchema() public view { - string memory schema = erc721GatedDiscount.productTypePriceSchema(); - - assertEq(schema, erc721GatedDiscount.productPriceSchema()); - } - function testProductTypePrice_NoDiscount() public { vm.prank(productOwner); erc721GatedDiscount.configureProductType(slicerId, productTypeId, abi.encode(new DiscountParams[](0))); @@ -178,21 +170,18 @@ contract NFTDiscountTest is RegistryProductPriceTest { 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)} - ); + 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); + 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); + newBasePrices = erc721GatedDiscount.slicerProductPrice(slicerId, ETH, buyer, productLineItems, basePrices); assertEq(newBasePrices[0], (basePrice - (basePrice * percentDiscountTwo) / 1e4)); } @@ -210,28 +199,19 @@ contract NFTDiscountTest is RegistryProductPriceTest { assertTrue(bytes(schema).length > 0); } - function testSlicerProductPriceSchema() public view { - string memory schema = erc721GatedDiscount.slicerProductPriceSchema(); - - assertEq(schema, erc721GatedDiscount.productPriceSchema()); - } - 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)} - ); + 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); + uint256[] memory newBasePrices = + erc721GatedDiscount.slicerProductPrice(slicerId, ETH, buyer3, productLineItems, basePrices); assertEq(newBasePrices[0], basePrice); } @@ -260,22 +240,20 @@ contract NFTDiscountTest is RegistryProductPriceTest { 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)} - ); + 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); + 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); - + newBasePrices = erc721GatedDiscount.slicerProductPrice(slicerId, ETH, buyer, productLineItems, basePrices); + assertEq(newBasePrices[0], basePrice); } From 198e1703bfcde0dfefcaf892b368198f88a3ff86 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 2 Nov 2025 18:47:53 -0300 Subject: [PATCH 26/30] fix hooks --- src/hooks/pricing/TieredDiscount/TieredDiscount.sol | 3 +-- test/pricing/TieredDiscount/NFTDiscount.t.sol | 13 +++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol index 505d30a..8f95990 100644 --- a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol @@ -72,8 +72,7 @@ abstract contract TieredDiscount is RegistryPrice { address, uint256 basePrice, uint256, - address buyer, - bytes memory + address buyer ) public view override returns (uint256) { DiscountParams[] memory discountParams = productTypeDiscounts[slicerId][productTypeId]; diff --git a/test/pricing/TieredDiscount/NFTDiscount.t.sol b/test/pricing/TieredDiscount/NFTDiscount.t.sol index 088d25c..520954a 100644 --- a/test/pricing/TieredDiscount/NFTDiscount.t.sol +++ b/test/pricing/TieredDiscount/NFTDiscount.t.sol @@ -67,15 +67,14 @@ contract NFTDiscountTest is RegistryProductPriceTest { uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - uint256 price = - erc721GatedDiscount.productTypePrice(slicerId, productTypeId, ETH, basePrice, quantity, buyer, ""); + 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, ""); + erc721GatedDiscount.productTypePrice(slicerId, productTypeId, ETH, basePrice, quantity, buyer); assertEq(priceWithHigherDiscount, (basePrice - (basePrice * percentDiscountTwo) / 1e4)); } @@ -98,8 +97,7 @@ contract NFTDiscountTest is RegistryProductPriceTest { 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, ""); + uint256 price = erc721GatedDiscount.productTypePrice(slicerId, productTypeId, ETH, basePrice, quantity, buyer2); assertEq(price, basePrice); } @@ -127,8 +125,7 @@ contract NFTDiscountTest is RegistryProductPriceTest { uint256 basePrice = PRODUCTS_MODULE.basePrice(slicerId, productId, variantId, ETH, quantity); - uint256 price = - erc721GatedDiscount.productTypePrice(slicerId, productTypeId, ETH, basePrice, quantity, buyer, ""); + uint256 price = erc721GatedDiscount.productTypePrice(slicerId, productTypeId, ETH, basePrice, quantity, buyer); assertEq(price, (basePrice - (basePrice * percentDiscountOne) / 1e4)); @@ -136,7 +133,7 @@ contract NFTDiscountTest is RegistryProductPriceTest { erc721GatedDiscount.configureProductType(slicerId, productTypeId, abi.encode(new DiscountParams[](0))); uint256 resetPrice = - erc721GatedDiscount.productTypePrice(slicerId, productTypeId, ETH, basePrice, quantity, buyer, ""); + erc721GatedDiscount.productTypePrice(slicerId, productTypeId, ETH, basePrice, quantity, buyer); assertEq(resetPrice, basePrice); } From 95922d49963f139608a275b8b8c64603dbe1d3fa Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 3 Nov 2025 09:53:03 -0300 Subject: [PATCH 27/30] update action hooks to support product type and slicer --- src/hooks/actions/ERC20Gated/ERC20Gated.sol | 105 +++++++++-- src/hooks/actions/ERC20Mint/ERC20Mint.sol | 166 +++++++++++++++--- src/hooks/actions/ERC721Mint/ERC721Mint.sol | 106 ++++++++--- src/hooks/actions/NFTGated/NFTGated.sol | 126 +++++++++---- .../NFTDiscount/NFTDiscount.sol | 72 ++++---- src/utils/RegistryAction.sol | 4 + 6 files changed, 446 insertions(+), 133 deletions(-) create mode 100644 src/utils/RegistryAction.sol diff --git a/src/hooks/actions/ERC20Gated/ERC20Gated.sol b/src/hooks/actions/ERC20Gated/ERC20Gated.sol index dfedaf1..78223a6 100644 --- a/src/hooks/actions/ERC20Gated/ERC20Gated.sol +++ b/src/hooks/actions/ERC20Gated/ERC20Gated.sol @@ -3,31 +3,36 @@ pragma solidity ^0.8.30; import {ERC20Gate} from "./types/ERC20Gate.sol"; import { + HookRegistry, IProductsModule, - RegistryProductAction, - ProductHookRegistry, + RegistryAction, IProductAction, - IProductHookRegistry -} 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 => 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 @@ -47,15 +52,37 @@ contract ERC20Gated is RegistryProductAction { ) public view override returns (bool) { ERC20Gate[] memory gates = tokenGates[slicerId][productId][variantId]; - 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 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 = productTypeTokenGates[slicerId][productTypeId]; + + return handleIsPurchaseAllowed(gates, buyer); + } + + /** + * @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); } /** @@ -66,13 +93,29 @@ contract ERC20Gated is RegistryProductAction { internal override { - (ERC20Gate[] memory gates) = abi.decode(params, (ERC20Gate[])); - delete tokenGates[slicerId][productId][variantId]; - for (uint256 i = 0; i < gates.length; i++) { - tokenGates[slicerId][productId][variantId].push(gates[i]); - } + handleConfigure(tokenGates[slicerId][productId][variantId], params); + } + + /** + * @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]; + + handleConfigure(productTypeTokenGates[slicerId][productTypeId], params); + } + + /** + * @inheritdoc HookRegistry + * @dev Sets the ERC20 gates for a slicer. + */ + function _configureSlicer(uint256 slicerId, bytes memory params) internal override { + delete slicerTokenGates[slicerId]; + + handleConfigure(slicerTokenGates[slicerId], params); } /** @@ -81,4 +124,28 @@ contract ERC20Gated is RegistryProductAction { 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 77d279d..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 { + HookRegistry, IProductsModule, - RegistryProductAction, - ProductHookRegistry, + RegistryAction, IProductAction, - IProductHookRegistry -} 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 //////////////////////////////////////////////////////////////*/ @@ -30,12 +33,14 @@ contract ERC20Mint is RegistryProductAction { 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 @@ -55,16 +60,50 @@ contract ERC20Mint is RegistryProductAction { ) public view virtual override returns (bool isAllowed) { 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. */ @@ -78,14 +117,43 @@ contract ERC20Mint is RegistryProductAction { ) internal override { 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); } /** @@ -96,6 +164,38 @@ contract ERC20Mint is RegistryProductAction { 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, @@ -108,7 +208,7 @@ contract ERC20Mint is RegistryProductAction { if (tokensPerUnit == 0) revert InvalidTokensPerUnit(); - ERC20Mint_BaseToken token = tokenData[slicerId][productId][variantId].token; + ERC20Mint_BaseToken token = tokenDataStorage.token; if (address(token) == address(0)) { token = new ERC20Mint_BaseToken(name, symbol, maxSupply); @@ -119,14 +219,32 @@ contract ERC20Mint is RegistryProductAction { token.setMaxSupply(maxSupply); } - tokenData[slicerId][productId][variantId] = ERC20Data(token, revertOnMaxSupplyReached, tokensPerUnit); + tokenDataStorage.token = token; + tokenDataStorage.revertOnMaxSupplyReached = revertOnMaxSupplyReached; + tokenDataStorage.tokensPerUnit = tokensPerUnit; } - /** - * @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"; + 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 ad697f0..de4d04f 100644 --- a/src/hooks/actions/ERC721Mint/ERC721Mint.sol +++ b/src/hooks/actions/ERC721Mint/ERC721Mint.sol @@ -1,12 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; -import { - IProductsModule, - RegistryProductAction, - ProductHookRegistry, - IProductHookRegistry -} 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"; @@ -16,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 //////////////////////////////////////////////////////////////*/ @@ -30,19 +26,21 @@ contract ERC721Mint is RegistryProductAction { 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. */ @@ -56,12 +54,43 @@ contract ERC721Mint is RegistryProductAction { ) internal override { 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); } /** @@ -72,6 +101,37 @@ contract ERC721Mint is RegistryProductAction { 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_, @@ -85,7 +145,7 @@ contract ERC721Mint is RegistryProductAction { if (royaltyFraction_ > MAX_ROYALTY) revert InvalidRoyaltyFraction(); - ERC721Mint_BaseToken token = tokenData[slicerId][productId][variantId].token; + ERC721Mint_BaseToken token = tokenDataStorage.token; if (address(token) == address(0)) { token = new ERC721Mint_BaseToken( @@ -95,14 +155,18 @@ contract ERC721Mint is RegistryProductAction { token.setParams(maxSupply, royaltyReceiver_, royaltyFraction_, baseURI__, tokenURI__); } - tokenData[slicerId][productId][variantId] = ERC721Data(token, revertOnMaxSupplyReached); + tokenDataStorage.token = token; + tokenDataStorage.revertOnMaxSupplyReached = revertOnMaxSupplyReached; } - /** - * @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"; + 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 b832671..cda8592 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 { + HookRegistry, IProductsModule, - RegistryProductAction, - ProductHookRegistry, + RegistryAction, IProductAction, - IProductHookRegistry -} 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,7 +20,7 @@ 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(); /*////////////////////////////////////////////////////////////// @@ -26,12 +29,14 @@ contract NFTGated is RegistryProductAction { 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 @@ -49,26 +54,33 @@ contract NFTGated is RegistryProductAction { uint256, bytes memory ) public view override returns (bool isAllowed) { - NFTGates memory nftGates_ = nftGates[slicerId][productId][variantId]; - - uint256 totalOwned; - unchecked { - for (uint256 i; i < nftGates_.gates.length;) { - NFTGate memory gate = nftGates_.gates[i]; - - if (gate.nftType == NftType.ERC1155) { - if (IERC1155(gate.nft).balanceOf(buyer, gate.id) >= gate.minQuantity) { - ++totalOwned; - } - } else if (IERC721(gate.nft).balanceOf(buyer) >= gate.minQuantity) { - ++totalOwned; - } + isAllowed = handleIsPurchaseAllowed(nftGates[slicerId][productId][variantId], buyer); + } - if (totalOwned >= nftGates_.minOwned) return true; + /** + * @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); + } - ++i; - } - } + /** + * @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) + { + isAllowed = handleIsPurchaseAllowed(slicerNftGates[slicerId], buyer); } /** @@ -79,11 +91,43 @@ contract NFTGated is RegistryProductAction { internal override { - (NFTGate[] memory gates, uint256 minOwned) = abi.decode(params, (NFTGate[], uint256)); - 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"; + } - nftGates[slicerId][productId][variantId].minOwned = 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)) { @@ -92,14 +136,30 @@ contract NFTGated is RegistryProductAction { } else if (!IERC1155(gates[i].nft).supportsInterface(type(IERC1155).interfaceId)) { revert InvalidNFT(); } - nftGates[slicerId][productId][variantId].gates.push(gates[i]); + gatesStorage.gates.push(gates[i]); } } - /** - * @inheritdoc IProductHookRegistry - */ - function configureProductSchema() public pure override returns (string memory) { - return "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] gates,uint256 minOwned"; + function handleIsPurchaseAllowed(NFTGates memory nftGates_, address buyer) internal view returns (bool) { + uint256 totalOwned; + unchecked { + for (uint256 i; i < nftGates_.gates.length;) { + NFTGate memory gate = nftGates_.gates[i]; + + if (gate.nftType == NftType.ERC1155) { + if (IERC1155(gate.nft).balanceOf(buyer, gate.id) >= gate.minQuantity) { + ++totalOwned; + } + } else if (IERC721(gate.nft).balanceOf(buyer) >= gate.minQuantity) { + ++totalOwned; + } + + if (totalOwned >= nftGates_.minOwned) return true; + + ++i; + } + } + + return false; } } diff --git a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol index fd2abbb..62affe8 100644 --- a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol @@ -24,39 +24,6 @@ contract NFTDiscount is TieredDiscount { CONFIGURATION //////////////////////////////////////////////////////////////*/ - function setProductDiscounts(DiscountParams[] storage storageDiscounts, bytes memory params) internal { - (DiscountParams[] memory newDiscounts) = abi.decode(params, (DiscountParams[])); - - uint256 prevDiscountValue; - DiscountParams memory discountParam; - for (uint256 i; i < newDiscounts.length;) { - discountParam = newDiscounts[i]; - - // Check relative discount doesn't exceed max value of 1e4 (100%) - if (discountParam.discount > 1e4) { - revert InvalidRelativeAmount(); - } - - if (discountParam.minQuantity == 0) { - revert InvalidMinQuantity(); - } - - // Check discounts are sorted in descending order - if (i > 0) { - if (discountParam.discount > prevDiscountValue) { - revert DiscountsNotDescending(discountParam); - } - } - prevDiscountValue = discountParam.discount; - - storageDiscounts.push(discountParam); - - unchecked { - ++i; - } - } - } - /** * @inheritdoc ProductHookRegistry * @notice Set base price and NFT discounts for a product. @@ -67,7 +34,7 @@ contract NFTDiscount is TieredDiscount { override { delete discounts[slicerId][productId][variantId]; - setProductDiscounts(discounts[slicerId][productId][variantId], params); + handleConfigure(discounts[slicerId][productId][variantId], params); } /** @@ -77,7 +44,7 @@ contract NFTDiscount is TieredDiscount { */ function _configureProductType(uint256 slicerId, uint256 productTypeId, bytes memory params) internal override { delete productTypeDiscounts[slicerId][productTypeId]; - setProductDiscounts(productTypeDiscounts[slicerId][productTypeId], params); + handleConfigure(productTypeDiscounts[slicerId][productTypeId], params); } /** @@ -87,7 +54,7 @@ contract NFTDiscount is TieredDiscount { */ function _configureSlicer(uint256 slicerId, bytes memory params) internal override { delete slicerDiscounts[slicerId]; - setProductDiscounts(slicerDiscounts[slicerId], params); + handleConfigure(slicerDiscounts[slicerId], params); } /** @@ -101,6 +68,39 @@ contract NFTDiscount is TieredDiscount { INTERNAL //////////////////////////////////////////////////////////////*/ + function handleConfigure(DiscountParams[] storage storageDiscounts, bytes memory params) internal { + (DiscountParams[] memory newDiscounts) = abi.decode(params, (DiscountParams[])); + + uint256 prevDiscountValue; + DiscountParams memory discountParam; + for (uint256 i; i < newDiscounts.length;) { + discountParam = newDiscounts[i]; + + // Check relative discount doesn't exceed max value of 1e4 (100%) + if (discountParam.discount > 1e4) { + revert InvalidRelativeAmount(); + } + + if (discountParam.minQuantity == 0) { + revert InvalidMinQuantity(); + } + + // Check discounts are sorted in descending order + if (i > 0) { + if (discountParam.discount > prevDiscountValue) { + revert DiscountsNotDescending(discountParam); + } + } + prevDiscountValue = discountParam.discount; + + storageDiscounts.push(discountParam); + + unchecked { + ++i; + } + } + } + /** * @inheritdoc TieredDiscount * @notice Base price is returned if user does not have a discount. 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"; From d1beda5f8a4d02857fce7010b6a927a401bc5167 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 3 Nov 2025 12:13:44 -0300 Subject: [PATCH 28/30] added product type and slicer action tests --- src/hooks/actions/NFTGated/NFTGated.sol | 5 + test/actions/ERC20Gated/ERC20Gated.t.sol | 233 ++++++++++++++ .../ERC20Gated/mocks/MockERC20Gated.sol | 8 + test/actions/ERC20Mint/ERC20Mint.t.sol | 244 +++++++++++++++ test/actions/ERC721Mint/ERC721Mint.t.sol | 291 ++++++++++++++++++ test/actions/NFTGated/NFTGated.t.sol | 247 +++++++++++++++ test/actions/NFTGated/mocks/MockNFTGated.sol | 16 + 7 files changed, 1044 insertions(+) diff --git a/src/hooks/actions/NFTGated/NFTGated.sol b/src/hooks/actions/NFTGated/NFTGated.sol index cda8592..65a82fb 100644 --- a/src/hooks/actions/NFTGated/NFTGated.sol +++ b/src/hooks/actions/NFTGated/NFTGated.sol @@ -141,6 +141,11 @@ contract NFTGated is RegistryAction { } 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 { for (uint256 i; i < nftGates_.gates.length;) { diff --git a/test/actions/ERC20Gated/ERC20Gated.t.sol b/test/actions/ERC20Gated/ERC20Gated.t.sol index 08e0eb5..92ba68f 100644 --- a/test/actions/ERC20Gated/ERC20Gated.t.sol +++ b/test/actions/ERC20Gated/ERC20Gated.t.sol @@ -5,11 +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; @@ -103,4 +106,234 @@ contract ERC20GatedTest is RegistryProductActionTest { 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 d5202a5..f6eedc8 100644 --- a/test/actions/ERC20Gated/mocks/MockERC20Gated.sol +++ b/test/actions/ERC20Gated/mocks/MockERC20Gated.sol @@ -9,4 +9,12 @@ contract MockERC20Gated is ERC20Gated { 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 0997895..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 {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; @@ -419,4 +422,245 @@ contract ERC20MintTest is RegistryProductActionTest { 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 c0df264..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 {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; @@ -483,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 54278fa..f65da29 100644 --- a/test/actions/NFTGated/NFTGated.t.sol +++ b/test/actions/NFTGated/NFTGated.t.sol @@ -8,10 +8,13 @@ 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 {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; @@ -128,6 +131,250 @@ contract NFTGatedTest is RegistryProductActionTest { 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(); + } + /*////////////////////////////////////////////////////////////// INTERNAL //////////////////////////////////////////////////////////////*/ diff --git a/test/actions/NFTGated/mocks/MockNFTGated.sol b/test/actions/NFTGated/mocks/MockNFTGated.sol index 619d849..2a2a89f 100644 --- a/test/actions/NFTGated/mocks/MockNFTGated.sol +++ b/test/actions/NFTGated/mocks/MockNFTGated.sol @@ -9,4 +9,20 @@ contract MockNFTGated is NFTGated { 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; + } } From b7af93fc3e54964c513c7778442c37333787e008 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 3 Nov 2025 18:01:02 -0300 Subject: [PATCH 29/30] nit --- script/ScriptUtils.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/ScriptUtils.sol b/script/ScriptUtils.sol index 7637102..3976d91 100644 --- a/script/ScriptUtils.sol +++ b/script/ScriptUtils.sol @@ -7,8 +7,8 @@ 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 {IProductHookRegistry} from "slice/interfaces/hooks/IHookRegistry/IProductHookRegistry.sol"; import { + IProductHookRegistry, IProductTypeHookRegistry, ISlicerHookRegistry, IProductPrice, From d22f277cc5804348289d28cf6fe5b7d484ff3770 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 9 Nov 2025 13:49:11 -0300 Subject: [PATCH 30/30] fix: generated-hooks-types --- deployments/addresses.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/deployments/addresses.json b/deployments/addresses.json index 03b7c8f..3111f86 100644 --- a/deployments/addresses.json +++ b/deployments/addresses.json @@ -19,8 +19,8 @@ "address": "0x247ebf608A0028DE933f1F90eE3920864D3fC621", "blockNumber": 36440973, "configureProductSchema": "(address erc20,uint256 amount)[] erc20Gates", - "configureProductTypeSchema": "not-supported", - "configureSlicerSchema": "not-supported", + "configureProductTypeSchema": "(address erc20,uint256 amount)[] erc20Gates", + "configureSlicerSchema": "(address erc20,uint256 amount)[] erc20Gates", "onProductPurchaseSchema": "", "productPriceSchema": "not-supported", "productTypePriceSchema": "not-supported", @@ -33,8 +33,8 @@ "address": "0x8B03B4dE7F6FD7A7BDD60Cb00aa49E144e45cdd0", "blockNumber": 36441450, "configureProductSchema": "string name,string symbol,uint256 premintAmount,address premintReceiver,bool revertOnMaxSupplyReached,uint256 maxSupply,uint256 tokensPerUnit", - "configureProductTypeSchema": "not-supported", - "configureSlicerSchema": "not-supported", + "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", @@ -47,8 +47,8 @@ "address": "0x48eAf12a1dC2a6dfACcb4C03F381B07ad5D5961C", "blockNumber": 36441490, "configureProductSchema": "string name,string symbol,address royaltyReceiver,uint256 royaltyFraction,string baseURI,string tokenURI,bool revertOnMaxSupplyReached,uint256 maxSupply", - "configureProductTypeSchema": "not-supported", - "configureSlicerSchema": "not-supported", + "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", @@ -61,8 +61,8 @@ "address": "0x560463f7bF3DFDB4Cff4f96c3399313d610Dd349", "blockNumber": 36441510, "configureProductSchema": "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] gates,uint256 minOwned", - "configureProductTypeSchema": "not-supported", - "configureSlicerSchema": "not-supported", + "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",