From ca5069a0e2a29395a1ae8b192702eb460287b888 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Fri, 29 Aug 2025 17:14:44 +0200 Subject: [PATCH 01/42] 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 dfa385b3c17e0c0a4dd1d65223aab1c1d12d33fa Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Tue, 9 Sep 2025 17:54:54 +0200 Subject: [PATCH 02/42] 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 2f9b8930fe53ed1311b1d9bb2a812094e2f4c7c6 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 14 Sep 2025 18:49:36 +0200 Subject: [PATCH 03/42] 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 958d748fb49ca2fa18e2cd77421ee979fa56c9e3 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 14 Sep 2025 19:11:42 +0200 Subject: [PATCH 04/42] 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 57f885a4919eb4ab4ac9ccbd4876845ea328150e Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 15 Sep 2025 01:52:44 +0200 Subject: [PATCH 05/42] 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 3bc370c66d5cd53cf2f8686050930e3dcfd7fe72 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 15 Sep 2025 02:26:01 +0200 Subject: [PATCH 06/42] 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 21f1775b49942ee1a1742c9cec270f5a35155a9a Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 17 Sep 2025 02:32:28 +0200 Subject: [PATCH 07/42] 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 84c75b4e7856eef8861dce140ca62539231fef07 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 22 Sep 2025 21:57:15 +0200 Subject: [PATCH 08/42] 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 115a4c30988dd4c1b9032f23c580826a68f48413 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Thu, 25 Sep 2025 00:43:54 +0200 Subject: [PATCH 09/42] 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 3e30f91070d3762cee88a1e11316b82de1185215 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 28 Sep 2025 19:56:37 +0200 Subject: [PATCH 10/42] 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 ba271afafad92a260f9f481b62680897af97b746 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 29 Sep 2025 02:44:31 +0200 Subject: [PATCH 11/42] 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 a484fd90fd95d0e63f0f3e9990104c58006250c9 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 29 Sep 2025 14:51:21 +0200 Subject: [PATCH 12/42] 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 a4afabfeb5c7f3d9a31bb92ee3070c3d8c9f46c2 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 29 Sep 2025 15:35:25 +0200 Subject: [PATCH 13/42] 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 737189b6f4556651e7ef15f92fd40d2ee5efe7e2 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 1 Oct 2025 02:55:53 +0200 Subject: [PATCH 14/42] 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 815a305e2384f177c16456d9329c8fce033633b3 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Thu, 2 Oct 2025 15:25:31 +0200 Subject: [PATCH 15/42] 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 5cf987ececcabd8701977d625ffa3231c1823d4c Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Thu, 2 Oct 2025 21:23:33 +0200 Subject: [PATCH 16/42] 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 20355c99cddc9d76a34bd032d72293914c706800 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Thu, 2 Oct 2025 21:53:08 +0200 Subject: [PATCH 17/42] 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 06235885d4509b6d3c8dccbff471072c3fb224f2 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 5 Oct 2025 01:00:04 +0200 Subject: [PATCH 18/42] 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 6f0ec46039893ff236ba4b50ee709a265abbd0bc Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 5 Oct 2025 02:12:10 +0200 Subject: [PATCH 19/42] 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 5d86972d8f0d7a216d78e5b57caabc7eac78c0e2 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 5 Oct 2025 14:52:04 +0200 Subject: [PATCH 20/42] 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 2a4622bd09e14fc8390990307643292a61572c4f Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 5 Oct 2025 15:56:43 +0200 Subject: [PATCH 21/42] 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 2336640a009e14fc24d118e502375a53606758c4 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 5 Oct 2025 17:58:29 +0200 Subject: [PATCH 22/42] 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 4d9d8c477021b8b2b7d19f98c2f63fdce701af33 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 5 Oct 2025 19:48:58 +0200 Subject: [PATCH 23/42] 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 1b0ee91aa82c2b9b9b743facf94815b6cd45c69b Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 29 Oct 2025 02:22:26 +0100 Subject: [PATCH 24/42] 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 038c1aa4e6bfe17ce2a22af3e8b020b42a64aacc Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 29 Oct 2025 03:23:13 +0100 Subject: [PATCH 25/42] 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 8d6c9354e5a718ae6d4af49a4581ed78fda0854f Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 2 Nov 2025 18:47:53 -0300 Subject: [PATCH 26/42] 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 26729b1d9171f817f2fca1703f20fee6b2b54317 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 3 Nov 2025 09:53:03 -0300 Subject: [PATCH 27/42] 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 81406b1f66408d956997070a267c76fc8f624b3f Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 3 Nov 2025 12:13:44 -0300 Subject: [PATCH 28/42] 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 3fdf67ae589ef6017fdba6ce7b2bff8fca6b1989 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 3 Nov 2025 18:01:02 -0300 Subject: [PATCH 29/42] 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 00cbac8ba35a87d52aad1bcfdc9b9895eca23dc1 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 9 Nov 2025 13:49:11 -0300 Subject: [PATCH 30/42] 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", From 9bd5f5a5d1d4672958d1ed9d95c62752acdf36a1 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 18 Mar 2026 20:38:09 +0100 Subject: [PATCH 31/42] feat: Payment and Order --- foundry.toml | 2 +- .../VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol | 15 ++++++++++----- .../LogisticVRGDAPrices/LogisticVRGDAPrices.sol | 15 ++++++++++----- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/foundry.toml b/foundry.toml index a8f247f..1410462 100644 --- a/foundry.toml +++ b/foundry.toml @@ -53,5 +53,5 @@ erc721a = "4.3.0" solady = "0.1.26" [lint] -ignore = ["test/**/*.sol","script/**/*.sol", "src/interfaces/*.sol", "src/utils/*.sol", "src/hooks/actions/actions.sol", "src/hooks/pricing/pricing.sol", "src/hooks/pricingActions/pricingActions.sol"] +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", "src/utils/math/SignedWadMath.sol"] exclude_lints=["mixed-case-function", "mixed-case-variable", "pascal-case-struct"] diff --git a/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol index adba315..96d7b42 100644 --- a/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol @@ -2,7 +2,10 @@ pragma solidity ^0.8.30; import { - ProductHookRegistry, IProductPrice, IProductHookRegistry, IProductsModule + ProductHookRegistry, + IProductPrice, + IProductHookRegistry, + IProductsModule } from "@/utils/RegistryProductPrice.sol"; import {wadLn, unsafeWadDiv, toDaysWadUnsafe} from "@/utils/math/SignedWadMath.sol"; import {LinearProduct} from "../types/LinearProduct.sol"; @@ -46,7 +49,7 @@ contract LinearVRGDAPrices is VRGDAPrices { require(decayConstant >= type(int184).min, "MIN_DECAY_CONSTANT_EXCEEDED"); /// Get product availability and isInfinite - (uint256 stockUnits_, bool isInfinite) = getStockUnits(slicerId, productId, 0); + (uint32 stockUnits_, bool isInfinite) = getStockUnits(slicerId, productId, 0); // Product must not have infinite availability require(!isInfinite, "NOT_FINITE_AVAILABILITY"); @@ -54,8 +57,8 @@ contract LinearVRGDAPrices is VRGDAPrices { // Set product params _productParams[slicerId][productId][variantId] = LinearProduct({ startTime: uint40(block.timestamp), - startUnits: uint32(stockUnits_), - decayConstant: int184(decayConstant), + startUnits: stockUnits_, + decayConstant: int184(decayConstant), // forge-lint: disable-line(unsafe-typecast) min: min, perTimeUnit: perTimeUnit }); @@ -82,11 +85,13 @@ contract LinearVRGDAPrices is VRGDAPrices { (uint256 stockUnits_,) = getStockUnits(slicerId, productId, variantId); uint256 basePrice_ = getBasePrice(slicerId, productId, variantId, currency, 1); + if (basePrice_ > uint256(type(int256).max)) revert("BASE_PRICE_TOO_HIGH"); + // Calculate sold units from stockUnits uint256 soldUnits = productParams.startUnits - stockUnits_; price = getAdjustedVRGDAPrice( - int256(basePrice_), + int256(basePrice_), // forge-lint: disable-line(unsafe-typecast) productParams.decayConstant, toDaysWadUnsafe(block.timestamp - productParams.startTime), soldUnits, diff --git a/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol b/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol index 8dcbb5c..8d70543 100644 --- a/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol @@ -2,7 +2,10 @@ pragma solidity ^0.8.30; import { - ProductHookRegistry, IProductPrice, IProductHookRegistry, IProductsModule + ProductHookRegistry, + IProductPrice, + IProductHookRegistry, + IProductsModule } from "@/utils/RegistryProductPrice.sol"; import { wadMul, @@ -55,7 +58,7 @@ contract LogisticVRGDAPrices is VRGDAPrices { require(decayConstant >= type(int184).min, "MIN_DECAY_CONSTANT_EXCEEDED"); // Get product availability and isInfinite - (uint256 stockUnits_, bool isInfinite) = getStockUnits(slicerId, productId, 0); + (uint32 stockUnits_, bool isInfinite) = getStockUnits(slicerId, productId, 0); // Product must not have infinite availability require(!isInfinite, "NON_FINITE_AVAILABILITY"); @@ -63,8 +66,8 @@ contract LogisticVRGDAPrices is VRGDAPrices { // Set product params _productParams[slicerId][productId][variantId] = LogisticProduct({ startTime: uint40(block.timestamp), - startUnits: uint32(stockUnits_), - decayConstant: int184(decayConstant), + startUnits: stockUnits_, + decayConstant: int184(decayConstant), // forge-lint: disable-line(unsafe-typecast) min: min, timeScale: timescale }); @@ -91,11 +94,13 @@ contract LogisticVRGDAPrices is VRGDAPrices { (uint256 stockUnits_,) = getStockUnits(slicerId, productId, variantId); uint256 basePrice_ = getBasePrice(slicerId, productId, variantId, currency, 1); + if (basePrice_ > uint256(type(int256).max)) revert("BASE_PRICE_TOO_HIGH"); + // Calculate sold units from stockUnits uint256 soldUnits = productParams.startUnits - stockUnits_; price = getAdjustedVRGDALogisticPrice( - int256(basePrice_), + int256(basePrice_), // forge-lint: disable-line(unsafe-typecast) productParams.decayConstant, toDaysWadUnsafe(block.timestamp - productParams.startTime), toWadUnsafe(productParams.startUnits + 1), From 15886ddc02b77108442199e4aee665a8c74aea2c Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 8 Apr 2026 19:24:34 +0200 Subject: [PATCH 32/42] nit: formatting --- foundry.toml | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/foundry.toml b/foundry.toml index 1410462..f458c9f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,7 +5,12 @@ optimizer_runs = 1000000 no_match_test = "FFI" dynamic_test_linking = true libs = ["dependencies", "../core/src", "../core/dependencies"] -fs_permissions = [{ access = "read", path = "./src"}, { access= "read", path = "./broadcast/Deploy.s.sol/8453/run-latest.json"}, { access = "read-write", path = "./deployments"}, { access = "read", path = "./out"}] +fs_permissions = [ + { access = "read", path = "./src" }, + { access = "read", path = "./broadcast/Deploy.s.sol/8453/run-latest.json" }, + { access = "read-write", path = "./deployments" }, + { access = "read", path = "./out" }, +] remappings = [ "slice/=dependencies/slice-0.0.9/", "@openzeppelin-4.8.0/=dependencies/@openzeppelin-contracts-4.8.0/", @@ -14,17 +19,17 @@ remappings = [ "@erc721a/=dependencies/erc721a-4.3.0/contracts/", "forge-std/=dependencies/forge-std-1.9.7/src/", "@test/=test/", - "@/=src/" + "@/=src/", ] [profile.ci] -fuzz_runs = 10000 +fuzz.runs = 10000 [profile.ffi] ffi = true match_test = "FFI" no_match_test = "a^" -fuzz_runs = 1000 +fuzz.runs = 1000 [rpc_endpoints] @@ -34,10 +39,10 @@ base = "${RPC_URL_BASE}" base-goerli = "${RPC_URL_BASE_GOERLI}" [etherscan] -mainnet = { key="${ETHERSCAN_KEY}", chain=1 } -op = { key="${ETHERSCAN_KEY}", chain=10 } -base = { key="${ETHERSCAN_KEY}", chain=8453, url="https://api.basescan.org/api" } -base-goerli = {key="${ETHERSCAN_KEY}", chain=84531, url="https://api-goerli.basescan.org/api" } +mainnet = { key = "${ETHERSCAN_KEY}", chain = 1 } +op = { key = "${ETHERSCAN_KEY}", chain = 10 } +base = { key = "${ETHERSCAN_KEY}", chain = 8453, url = "https://api.basescan.org/api" } +base-goerli = { key = "${ETHERSCAN_KEY}", chain = 84531, url = "https://api-goerli.basescan.org/api" } [soldeer] recursive_deps = true @@ -53,5 +58,18 @@ erc721a = "4.3.0" solady = "0.1.26" [lint] -ignore = ["test/**/*.sol","script/**/*.sol", "src/interfaces/*.sol", "src/utils/*.sol", "src/hooks/actions/actions.sol", "src/hooks/pricing/pricing.sol", "src/hooks/pricingActions/pricingActions.sol", "src/utils/math/SignedWadMath.sol"] -exclude_lints=["mixed-case-function", "mixed-case-variable", "pascal-case-struct"] +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", + "src/utils/math/SignedWadMath.sol", +] +exclude_lints = [ + "mixed-case-function", + "mixed-case-variable", + "pascal-case-struct", +] From 9d6a36943903aab4c0dbea99afe2651f80e16519 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Thu, 7 May 2026 22:33:40 +0200 Subject: [PATCH 33/42] fixes --- src/hooks/pricing/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/pricing/README.md b/src/hooks/pricing/README.md index acea57c..5bf171d 100644 --- a/src/hooks/pricing/README.md +++ b/src/hooks/pricing/README.md @@ -5,7 +5,7 @@ Calculate dynamic prices for products on Slice. Pricing strategies implement the ## How Pricing Works The `productPrice` function is called before purchase to determine: -- **ETH price** - Price in native currency +- **ETH price** - Price in ETH - **Currency price** - Price in ERC20 tokens (if applicable) ## Creating Custom Pricing @@ -46,4 +46,4 @@ The generator script will also create a test file for your pricing strategy. Cus ## Best Practices - Ensure the returned price adapts based on the quantity. -- Typically either ETH or currency price is returned, but you can return both if needed. \ No newline at end of file +- Typically either ETH or currency price is returned, but you can return both if needed. From e929bd4de2c2977a96278f8978a086213788df87 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 10 Jun 2026 13:18:36 +0200 Subject: [PATCH 34/42] fix --- foundry.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/foundry.toml b/foundry.toml index f458c9f..3a1b746 100644 --- a/foundry.toml +++ b/foundry.toml @@ -22,9 +22,6 @@ remappings = [ "@/=src/", ] -[profile.ci] -fuzz.runs = 10000 - [profile.ffi] ffi = true match_test = "FFI" From fafbdfd251e9c7719ea9ff485962f660f7fb6313 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 10 Jun 2026 13:27:33 +0200 Subject: [PATCH 35/42] foundry toml --- foundry.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/foundry.toml b/foundry.toml index 3a1b746..a597cf7 100644 --- a/foundry.toml +++ b/foundry.toml @@ -22,6 +22,9 @@ remappings = [ "@/=src/", ] +[profile.ci] +fuzz.runs = 10000 + [profile.ffi] ffi = true match_test = "FFI" @@ -56,6 +59,7 @@ solady = "0.1.26" [lint] ignore = [ + "dependencies/**/*.sol", "test/**/*.sol", "script/**/*.sol", "src/interfaces/*.sol", From 3a805a8456cd0da052482baa654b7f291a01886b Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 10 Jun 2026 13:52:29 +0200 Subject: [PATCH 36/42] fix: foundry warnings in CI --- foundry.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/foundry.toml b/foundry.toml index a597cf7..36afb5a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,6 +5,7 @@ optimizer_runs = 1000000 no_match_test = "FFI" dynamic_test_linking = true libs = ["dependencies", "../core/src", "../core/dependencies"] +ignored_warnings_from = ["dependencies", "../core/dependencies"] fs_permissions = [ { access = "read", path = "./src" }, { access = "read", path = "./broadcast/Deploy.s.sol/8453/run-latest.json" }, @@ -60,6 +61,7 @@ solady = "0.1.26" [lint] ignore = [ "dependencies/**/*.sol", + "../core/dependencies/**/*.sol", "test/**/*.sol", "script/**/*.sol", "src/interfaces/*.sol", From 55a3b0cfd80bdbdf4207d2cd2c5480cac4888da8 Mon Sep 17 00:00:00 2001 From: Dom Mac Date: Thu, 18 Jun 2026 18:13:33 +0200 Subject: [PATCH 37/42] contracts envs --- .env.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index c9a9206..20d75d5 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,8 @@ RPC_URL_MAINNET= RPC_URL_OPTIMISM= RPC_URL_BASE= RPC_URL_BASE_GOERLI= +RPC_URL_8453= ETHERSCAN_KEY= -PRIVATE_KEY= \ No newline at end of file +PRIVATE_KEY= From 027ca7459a4ab8c4a0ebad7d20e1d19e88bea41b Mon Sep 17 00:00:00 2001 From: Dom Mac Date: Thu, 18 Jun 2026 18:53:43 +0200 Subject: [PATCH 38/42] refactor and comments --- .env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 20d75d5..eaabfc8 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,8 @@ RPC_URL_MAINNET= RPC_URL_OPTIMISM= -RPC_URL_BASE= +RPC_URL_BASE= # global/shared Base RPC URL RPC_URL_BASE_GOERLI= -RPC_URL_8453= +RPC_URL_8453= # global/shared Base RPC URL alias ETHERSCAN_KEY= From a06f80ba42bb84bf46362920ee043f02119defe8 Mon Sep 17 00:00:00 2001 From: Dom Mac Date: Thu, 18 Jun 2026 19:00:26 +0200 Subject: [PATCH 39/42] refactor and comments --- .env.example | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.env.example b/.env.example index eaabfc8..07f6ca4 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,12 @@ +# Scope: RPC endpoints. RPC_URL_MAINNET= RPC_URL_OPTIMISM= RPC_URL_BASE= # global/shared Base RPC URL RPC_URL_BASE_GOERLI= RPC_URL_8453= # global/shared Base RPC URL alias +# Scope: explorer verification. ETHERSCAN_KEY= +# Scope: signer keys. PRIVATE_KEY= From cd8561cf05f3839a6dd2566e04b375932606ec9a Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Fri, 7 Aug 2026 16:59:36 +0200 Subject: [PATCH 40/42] Move walletRecovery hono endpoint from indexer to api --- foundry.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/foundry.toml b/foundry.toml index 36afb5a..68a145f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -72,6 +72,7 @@ ignore = [ "src/utils/math/SignedWadMath.sol", ] exclude_lints = [ + "block-timestamp", "mixed-case-function", "mixed-case-variable", "pascal-case-struct", From 98eb7feecc1235319f49a2ab0dba3b44f2109f4e Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 19 Aug 2026 02:21:45 +0200 Subject: [PATCH 41/42] update licences --- LICENSE | 12 ++++++------ README.md | 6 +++++- script/Seed.s.sol | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/LICENSE b/LICENSE index 4bd79c7..ca12056 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -MIT License +The MIT License (MIT) -Copyright (c) 2025 Slice +Copyright (c) Slice Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index a861291..5b3d3f9 100644 --- a/README.md +++ b/README.md @@ -158,4 +158,8 @@ src/ - [Actions Guide](./src/hooks/actions/README.md) - [Pricing Strategies Guide](./src/hooks/pricing/README.md) - [Pricing + Actions Guide](./src/hooks/pricingActions/README.md) -- [Custom Implementations](./src/custom/README.md) \ No newline at end of file +- [Custom Implementations](./src/custom/README.md) + +## License + +This repository is available under the [MIT License](./LICENSE). diff --git a/script/Seed.s.sol b/script/Seed.s.sol index fb3b45f..208c591 100644 --- a/script/Seed.s.sol +++ b/script/Seed.s.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import {Script} from "forge-std/Script.sol"; From 1b83fe69cfb45b2507e9a14884dd8b2888346629 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Thu, 20 Aug 2026 21:07:13 +0200 Subject: [PATCH 42/42] Refactor application architecture and remove obsolete code --- .env.example | 8 ++------ foundry.toml | 7 +++---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index 07f6ca4..a299b74 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,5 @@ -# Scope: RPC endpoints. -RPC_URL_MAINNET= -RPC_URL_OPTIMISM= -RPC_URL_BASE= # global/shared Base RPC URL -RPC_URL_BASE_GOERLI= -RPC_URL_8453= # global/shared Base RPC URL alias +# Scope: RPC provider credential. +SLICEGLOBAL_INTERNAL_ALCHEMY_ID= # Scope: explorer verification. ETHERSCAN_KEY= diff --git a/foundry.toml b/foundry.toml index 68a145f..dfc088d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -34,10 +34,9 @@ fuzz.runs = 1000 [rpc_endpoints] -mainnet = "${RPC_URL_MAINNET}" -op = "${RPC_URL_OPTIMISM}" -base = "${RPC_URL_BASE}" -base-goerli = "${RPC_URL_BASE_GOERLI}" +mainnet = "https://eth-mainnet.g.alchemy.com/v2/${SLICEGLOBAL_INTERNAL_ALCHEMY_ID}" +op = "https://opt-mainnet.g.alchemy.com/v2/${SLICEGLOBAL_INTERNAL_ALCHEMY_ID}" +base = "https://base-mainnet.g.alchemy.com/v2/${SLICEGLOBAL_INTERNAL_ALCHEMY_ID}" [etherscan] mainnet = { key = "${ETHERSCAN_KEY}", chain = 1 }