From a59fc8e996d7a6fa9da45ca16144543a978e6854 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 23 Jul 2025 20:59:38 +0200 Subject: [PATCH 01/24] wip --- deployments/addresses.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deployments/addresses.json b/deployments/addresses.json index fe65f1e..a3637d7 100644 --- a/deployments/addresses.json +++ b/deployments/addresses.json @@ -1,5 +1,5 @@ { "actions": {}, - "pricingStrategies": {}, - "pricingStrategyActions": {} + "pricing": {}, + "pricingActions": {} } From 98f32d90b1140a04d9a9825e7c933fc737e70ceb Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 28 Jul 2025 20:22:55 +0200 Subject: [PATCH 02/24] hook deployments --- deployments/addresses.json | 30 +++++++++++++++++-- .../ERC721Mint.sol | 0 .../types/ERC721Data.sol | 0 .../utils/ERC721Mint_BaseToken.sol | 0 4 files changed, 27 insertions(+), 3 deletions(-) rename src/hooks/actions/{ERC721AMint => ERC721Mint}/ERC721Mint.sol (100%) rename src/hooks/actions/{ERC721AMint => ERC721Mint}/types/ERC721Data.sol (100%) rename src/hooks/actions/{ERC721AMint => ERC721Mint}/utils/ERC721Mint_BaseToken.sol (100%) diff --git a/deployments/addresses.json b/deployments/addresses.json index a3637d7..0880f50 100644 --- a/deployments/addresses.json +++ b/deployments/addresses.json @@ -1,5 +1,29 @@ { - "actions": {}, - "pricing": {}, - "pricingActions": {} + "actions": { + "Allowlisted": [ + { + "address": "0x2C200B02dC80fD3355eE50232fF795111e48F86E", + "blockNumber": 33461146, + "transactionHash": "0xbd164ba876ae612749895972c4d4ee50f10993790aa642ed02e0a46c4ddfb10f" + } + ] + }, + "pricing": { + "NFTDiscount": [ + { + "address": "0x306aD63c2f4D07c9BFC6F09DA8bC726cd3Ff4C92", + "blockNumber": 33461167, + "transactionHash": "0xcc4f5eaa27e6e5dd06fbfb26e832a45e0a104e1ab533ea67a1f14699fc088426" + } + ] + }, + "pricingActions": { + "FirstForFree": [ + { + "address": "0xECa75B5519ab1F60Bb047F49D80A9137ccEC3C4e", + "blockNumber": 33461563, + "transactionHash": "0x829d28f33f015856504b4cfeb03305ba44e88548cf3372f7211ef3cfeede6902" + } + ] + } } diff --git a/src/hooks/actions/ERC721AMint/ERC721Mint.sol b/src/hooks/actions/ERC721Mint/ERC721Mint.sol similarity index 100% rename from src/hooks/actions/ERC721AMint/ERC721Mint.sol rename to src/hooks/actions/ERC721Mint/ERC721Mint.sol diff --git a/src/hooks/actions/ERC721AMint/types/ERC721Data.sol b/src/hooks/actions/ERC721Mint/types/ERC721Data.sol similarity index 100% rename from src/hooks/actions/ERC721AMint/types/ERC721Data.sol rename to src/hooks/actions/ERC721Mint/types/ERC721Data.sol diff --git a/src/hooks/actions/ERC721AMint/utils/ERC721Mint_BaseToken.sol b/src/hooks/actions/ERC721Mint/utils/ERC721Mint_BaseToken.sol similarity index 100% rename from src/hooks/actions/ERC721AMint/utils/ERC721Mint_BaseToken.sol rename to src/hooks/actions/ERC721Mint/utils/ERC721Mint_BaseToken.sol From 8d507d752202363a1b5a20d27f550e6dfea00111 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Tue, 29 Jul 2025 01:04:42 +0200 Subject: [PATCH 03/24] fix --- test/actions/{ERC721AMint => ERC721Mint}/ERC721Mint.t.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename test/actions/{ERC721AMint => ERC721Mint}/ERC721Mint.t.sol (98%) diff --git a/test/actions/ERC721AMint/ERC721Mint.t.sol b/test/actions/ERC721Mint/ERC721Mint.t.sol similarity index 98% rename from test/actions/ERC721AMint/ERC721Mint.t.sol rename to test/actions/ERC721Mint/ERC721Mint.t.sol index f76a30c..c040a75 100644 --- a/test/actions/ERC721AMint/ERC721Mint.t.sol +++ b/test/actions/ERC721Mint/ERC721Mint.t.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.20; import {RegistryOnchainAction, RegistryOnchainActionTest} from "@test/utils/RegistryOnchainActionTest.sol"; -import {ERC721Mint} from "@/hooks/actions/ERC721AMint/ERC721Mint.sol"; -import {ERC721Data} from "@/hooks/actions/ERC721AMint/types/ERC721Data.sol"; -import {ERC721Mint_BaseToken, MAX_ROYALTY} from "@/hooks/actions/ERC721AMint/utils/ERC721Mint_BaseToken.sol"; +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"; From 97140d9a0cf5419f3f4feb15073246df8510b999 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Tue, 29 Jul 2025 20:08:34 +0200 Subject: [PATCH 04/24] add paramsSchema to hooks addresses.json --- deployments/addresses.json | 69 +++++++++++++++++++++++++++++++++----- script/ScriptUtils.sol | 15 ++++++--- script/deploy.sh | 2 +- 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/deployments/addresses.json b/deployments/addresses.json index 0880f50..ca2d364 100644 --- a/deployments/addresses.json +++ b/deployments/addresses.json @@ -2,27 +2,78 @@ "actions": { "Allowlisted": [ { - "address": "0x2C200B02dC80fD3355eE50232fF795111e48F86E", - "blockNumber": 33461146, - "transactionHash": "0xbd164ba876ae612749895972c4d4ee50f10993790aa642ed02e0a46c4ddfb10f" + "address": "0x157428DD791E03c20880D22C3dA2B66A36B5cF26", + "blockNumber": 33510607, + "paramsSchema": "(address nft,uint8 tokenType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned", + "transactionHash": "0x6af9ac700f1c9a38de57fa4bc13262162d9b674649fd417ccd50237b6cfbc178" + } + ], + "ERC20Gated": [ + { + "address": "0x1Fe39fC12b4A320fBff17950392DdaE202DbAaC8", + "blockNumber": 33511005, + "paramsSchema": "(address nft,uint8 tokenType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned", + "transactionHash": "0x89cebfea9fdbba38c945368d9cd3decd83d35f43ff6f8f4ad9a8c1854b5a00db" + } + ], + "ERC20Mint": [ + { + "address": "0x776bc0e11FCfB0fE22987a7E45f9C2e1cF81aDFd", + "blockNumber": 33511032, + "paramsSchema": "(address nft,uint8 tokenType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned", + "transactionHash": "0x19614e531b98c4f691054614cdc804f668efafc1254dbb91f7c4a7ad230131b8" + } + ], + "ERC721Mint": [ + { + "address": "0x2b6488115FAa50142E140172CbCd60e6370675F7", + "blockNumber": 33511082, + "paramsSchema": "(address nft,uint8 tokenType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned", + "transactionHash": "0x4981b3b67c0b8abe6a942b3ca86643f2b1dfdbcce59d6c819ad20a82814956e0" + } + ], + "NFTGated": [ + { + "address": "0x6256e02F3A2aa4aD8B1234Fe027AaaAF44D7B8A2", + "blockNumber": 33511120, + "paramsSchema": "(address nft,uint8 tokenType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned", + "transactionHash": "0x0e20ee6ea9eae15bff648936dcefaf23a2b5f2a6c86547f2f4f8c50280803dc0" } ] }, "pricing": { "NFTDiscount": [ { - "address": "0x306aD63c2f4D07c9BFC6F09DA8bC726cd3Ff4C92", - "blockNumber": 33461167, - "transactionHash": "0xcc4f5eaa27e6e5dd06fbfb26e832a45e0a104e1ab533ea67a1f14699fc088426" + "address": "0xEaa74b2527a0693Fa49d6D0aC211E4cc777679a6", + "blockNumber": 33511167, + "paramsSchema": "uint256 usdcPrice,address token,uint88 tokenId,uint8 tokenType", + "transactionHash": "0x91a5b485b3258a012d782c2ebc3577da81ec7fb4b3eca09af47e609009fea4dd" + } + ], + "LinearVRGDAPrices": [ + { + "address": "0xEC68E30182F4298b7032400B7ce809da613e4449", + "blockNumber": 33511188, + "paramsSchema": "uint256 usdcPrice,address token,uint88 tokenId,uint8 tokenType", + "transactionHash": "0xf2667ce20d07561e59c8d3e3de135bbd895c5b08bb57fe487c6c8197c91a0d73" + } + ], + "LogisticVRGDAPrices": [ + { + "address": "0x2b02cC8528EF18abf8185543CEC29A94F0542c8F", + "blockNumber": 33511209, + "paramsSchema": "uint256 usdcPrice,address token,uint88 tokenId,uint8 tokenType", + "transactionHash": "0x2b27380661a38b54dbccf634305622296034ddfccf5865a07fbfb7810ea41025" } ] }, "pricingActions": { "FirstForFree": [ { - "address": "0xECa75B5519ab1F60Bb047F49D80A9137ccEC3C4e", - "blockNumber": 33461563, - "transactionHash": "0x829d28f33f015856504b4cfeb03305ba44e88548cf3372f7211ef3cfeede6902" + "address": "0x2C18D37b8229233F672bF406bCe8799BCfD43B5A", + "blockNumber": 33510960, + "paramsSchema": "uint256 usdcPrice,(address tokenAddress,uint8 tokenType,uint88 tokenId,uint8 minQuantity)[] eligibleTokens,address mintToken,uint88 mintTokenId,uint8 freeUnits", + "transactionHash": "0x8bd0b9de8edd704899210f8450096adf7f4f853030a7ba0a2f64494bc1ba726e" } ] } diff --git a/script/ScriptUtils.sol b/script/ScriptUtils.sol index d604d0c..4c0a8f0 100644 --- a/script/ScriptUtils.sol +++ b/script/ScriptUtils.sol @@ -7,6 +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"; /** * Helper contract to enforce correct chain selection in scripts @@ -84,6 +85,7 @@ abstract contract SetUpContractsList is Script { struct ContractDeploymentData { address contractAddress; uint256 blockNumber; + string paramsSchema; bytes32 transactionHash; } @@ -210,6 +212,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()); json[0] = vm.serializeBytes32("0", "transactionHash", transaction.hash); for (uint256 i = 0; i < existingContractAddresses.length; i++) { @@ -218,12 +221,14 @@ abstract contract SetUpContractsList is Script { vm.serializeAddress(index, "address", existingContractAddress.contractAddress); vm.serializeUint(index, "blockNumber", existingContractAddress.blockNumber); + vm.serializeString(index, "paramsSchema", existingContractAddress.paramsSchema); json[i + 1] = vm.serializeBytes32(index, "transactionHash", existingContractAddress.transactionHash); } } else { json = new string[](1); vm.serializeAddress("0", "address", transaction.contractAddress); vm.serializeUint("0", "blockNumber", receipt.blockNumber); + vm.serializeString("0", "paramsSchema", IHookRegistry(transaction.contractAddress).paramsSchema()); json[0] = vm.serializeBytes32("0", "transactionHash", transaction.hash); } } @@ -411,7 +416,7 @@ abstract contract SetUpContractsList is Script { break; } } - + // For hooks subdirectories, use the subdirectory name as the category if (foundSrc) { // Look for "hooks/" after src @@ -419,7 +424,7 @@ abstract contract SetUpContractsList is Script { while (hooksStart < pathBytes.length && pathBytes[hooksStart] == 0x2f) { hooksStart++; } - + // Check if path starts with "hooks/" bytes memory hooksBytes = bytes("hooks"); bool isHooksPath = true; @@ -437,7 +442,7 @@ abstract contract SetUpContractsList is Script { } else { isHooksPath = false; } - + if (isHooksPath) { // Find the subdirectory after "hooks/" uint256 subStart = hooksStart + hooksBytes.length + 1; // +1 for the slash @@ -448,7 +453,7 @@ abstract contract SetUpContractsList is Script { while (subEnd < pathBytes.length && pathBytes[subEnd] != 0x2f) { subEnd++; } - + if (subEnd > subStart) { bytes memory subFolderBytes = new bytes(subEnd - subStart); for (uint256 i = 0; i < subEnd - subStart; i++) { @@ -482,7 +487,7 @@ abstract contract SetUpContractsList is Script { } else { firstFolderName = CONTRACT_PATH; } - + // Now get the last folder as before for (uint256 i = 0; i < pathBytes.length; i++) { if (pathBytes[i] == "/") { diff --git a/script/deploy.sh b/script/deploy.sh index 71f9d6e..1016ba9 100755 --- a/script/deploy.sh +++ b/script/deploy.sh @@ -11,6 +11,6 @@ else forge script script/Deploy.s.sol --chain base --rpc-url base --private-key $PRIVATE_KEY --sig "run(string memory contractName)" "$contractName" --verify -vvvv --broadcast --slow fi -forge script script/WriteAddresses.s.sol --sig "run(string memory contractName)" "$contractName" +forge script script/WriteAddresses.s.sol --sig "run(string memory contractName)" "$contractName" --chain base --rpc-url base echo "Deployed contract: $contractName" \ No newline at end of file From 4fad537f2948c41d02a887b59889fb1817ab7a66 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Tue, 29 Jul 2025 20:24:39 +0200 Subject: [PATCH 05/24] fix: address generation script --- deployments/addresses.json | 26 +++++++++---------- script/ScriptUtils.sol | 1 + .../NFTDiscount/NFTDiscount.sol | 2 +- .../{NFTDiscount.sol => NFTDiscount.t.sol} | 0 4 files changed, 15 insertions(+), 14 deletions(-) rename test/pricingStrategies/TieredDiscount/{NFTDiscount.sol => NFTDiscount.t.sol} (100%) diff --git a/deployments/addresses.json b/deployments/addresses.json index ca2d364..c0e5e3f 100644 --- a/deployments/addresses.json +++ b/deployments/addresses.json @@ -4,7 +4,7 @@ { "address": "0x157428DD791E03c20880D22C3dA2B66A36B5cF26", "blockNumber": 33510607, - "paramsSchema": "(address nft,uint8 tokenType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned", + "paramsSchema": "bytes32 merkleRoot", "transactionHash": "0x6af9ac700f1c9a38de57fa4bc13262162d9b674649fd417ccd50237b6cfbc178" } ], @@ -12,7 +12,7 @@ { "address": "0x1Fe39fC12b4A320fBff17950392DdaE202DbAaC8", "blockNumber": 33511005, - "paramsSchema": "(address nft,uint8 tokenType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned", + "paramsSchema": "(address erc20,uint256 amount)[] erc20Gates", "transactionHash": "0x89cebfea9fdbba38c945368d9cd3decd83d35f43ff6f8f4ad9a8c1854b5a00db" } ], @@ -20,7 +20,7 @@ { "address": "0x776bc0e11FCfB0fE22987a7E45f9C2e1cF81aDFd", "blockNumber": 33511032, - "paramsSchema": "(address nft,uint8 tokenType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned", + "paramsSchema": "string name,string symbol,uint256 premintAmount,address premintReceiver,uint256 maxSupply,uint256 tokensPerUnit", "transactionHash": "0x19614e531b98c4f691054614cdc804f668efafc1254dbb91f7c4a7ad230131b8" } ], @@ -28,33 +28,33 @@ { "address": "0x2b6488115FAa50142E140172CbCd60e6370675F7", "blockNumber": 33511082, - "paramsSchema": "(address nft,uint8 tokenType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned", + "paramsSchema": "string name,string symbol,address royaltyReceiver,uint256 royaltyFraction,string baseURI,string tokenURI,bool revertOnMaxSupplyReached,uint256 maxSupply", "transactionHash": "0x4981b3b67c0b8abe6a942b3ca86643f2b1dfdbcce59d6c819ad20a82814956e0" } ], "NFTGated": [ { - "address": "0x6256e02F3A2aa4aD8B1234Fe027AaaAF44D7B8A2", - "blockNumber": 33511120, + "address": "0xDf0ed657bdf9e8475D2Ec36F801Bf2cDaacd9188", + "blockNumber": 33511957, "paramsSchema": "(address nft,uint8 tokenType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned", - "transactionHash": "0x0e20ee6ea9eae15bff648936dcefaf23a2b5f2a6c86547f2f4f8c50280803dc0" + "transactionHash": "0xaa9c235611578cc635e7bc072a8de65e99a56c391c5b76939604856957c59cd1" } ] }, "pricing": { "NFTDiscount": [ { - "address": "0xEaa74b2527a0693Fa49d6D0aC211E4cc777679a6", - "blockNumber": 33511167, - "paramsSchema": "uint256 usdcPrice,address token,uint88 tokenId,uint8 tokenType", - "transactionHash": "0x91a5b485b3258a012d782c2ebc3577da81ec7fb4b3eca09af47e609009fea4dd" + "address": "0x2Bc8DF056B6962C8A534aeD9eeBC7d21011E1E71", + "blockNumber": 33512225, + "paramsSchema": "(address currency,uint240 basePrice,bool isFree,uint8 discountType,(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[]discounts)[] allCurrencyParams", + "transactionHash": "0xe45ebfd62cc49d6a0c19e232e6351e8c1209fb64ea079524cd8d8a848b5a1c4b" } ], "LinearVRGDAPrices": [ { "address": "0xEC68E30182F4298b7032400B7ce809da613e4449", "blockNumber": 33511188, - "paramsSchema": "uint256 usdcPrice,address token,uint88 tokenId,uint8 tokenType", + "paramsSchema": "(address currency,int128 targetPrice,uint128 min,int256 perTimeUnit)[] linearParams,int256 priceDecayPercent", "transactionHash": "0xf2667ce20d07561e59c8d3e3de135bbd895c5b08bb57fe487c6c8197c91a0d73" } ], @@ -62,7 +62,7 @@ { "address": "0x2b02cC8528EF18abf8185543CEC29A94F0542c8F", "blockNumber": 33511209, - "paramsSchema": "uint256 usdcPrice,address token,uint88 tokenId,uint8 tokenType", + "paramsSchema": "(address currency,int128 targetPrice,uint128 min,int256 timeScale)[] logisticParams,int256 priceDecayPercent", "transactionHash": "0x2b27380661a38b54dbccf634305622296034ddfccf5865a07fbfb7810ea41025" } ] diff --git a/script/ScriptUtils.sol b/script/ScriptUtils.sol index 4c0a8f0..5d612f6 100644 --- a/script/ScriptUtils.sol +++ b/script/ScriptUtils.sol @@ -136,6 +136,7 @@ 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); arrStrings[j] = vm.serializeBytes32(idx, "transactionHash", existingData[j].transactionHash); } diff --git a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol index 5ac9380..f2b1e3b 100644 --- a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol @@ -106,7 +106,7 @@ contract NFTDiscount is TieredDiscount { */ function paramsSchema() external pure override returns (string memory) { return - "(address currency,uint240 basePrice,bool isFree,uint8 discountType,(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[] discounts)[] allCurrencyParams"; + "(address currency,uint240 basePrice,bool isFree,uint8 discountType,(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[]discounts)[] allCurrencyParams"; } /** diff --git a/test/pricingStrategies/TieredDiscount/NFTDiscount.sol b/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol similarity index 100% rename from test/pricingStrategies/TieredDiscount/NFTDiscount.sol rename to test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol From d04926fb2e0835f42693ff67106e2939cb153549 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 30 Jul 2025 22:24:36 +0200 Subject: [PATCH 06/24] update actions logic --- deployments/addresses.json | 20 +++++------ src/hooks/actions/ERC20Gated/ERC20Gated.sol | 2 ++ src/hooks/actions/ERC20Mint/ERC20Mint.sol | 2 +- src/hooks/actions/NFTGated/NFTGated.sol | 2 ++ test/actions/ERC20Gated/ERC20Gated.t.sol | 34 +++++++++++++++++-- .../ERC20Gated/mocks/MockERC20Gated.sol | 12 +++++++ test/actions/NFTGated/NFTGated.t.sol | 24 +++++++++++-- test/actions/NFTGated/mocks/MockNFTGated.sol | 12 +++++++ 8 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 test/actions/ERC20Gated/mocks/MockERC20Gated.sol create mode 100644 test/actions/NFTGated/mocks/MockNFTGated.sol diff --git a/deployments/addresses.json b/deployments/addresses.json index c0e5e3f..d2eb4fa 100644 --- a/deployments/addresses.json +++ b/deployments/addresses.json @@ -10,18 +10,18 @@ ], "ERC20Gated": [ { - "address": "0x1Fe39fC12b4A320fBff17950392DdaE202DbAaC8", - "blockNumber": 33511005, + "address": "0x26A1C86B555013995Fc72864D261fDe984752E7c", + "blockNumber": 33558792, "paramsSchema": "(address erc20,uint256 amount)[] erc20Gates", - "transactionHash": "0x89cebfea9fdbba38c945368d9cd3decd83d35f43ff6f8f4ad9a8c1854b5a00db" + "transactionHash": "0x3a7c01ede05a34280073479d5cdf1f35e41d9f08c36a71f358b9c503ccc54526" } ], "ERC20Mint": [ { - "address": "0x776bc0e11FCfB0fE22987a7E45f9C2e1cF81aDFd", - "blockNumber": 33511032, - "paramsSchema": "string name,string symbol,uint256 premintAmount,address premintReceiver,uint256 maxSupply,uint256 tokensPerUnit", - "transactionHash": "0x19614e531b98c4f691054614cdc804f668efafc1254dbb91f7c4a7ad230131b8" + "address": "0x67f9799FaC1D53C63217BEE47f553150F5BB0836", + "blockNumber": 33520592, + "paramsSchema": "string name,string symbol,uint256 premintAmount,address premintReceiver,bool revertOnMaxSupplyReached,uint256 maxSupply,uint256 tokensPerUnit", + "transactionHash": "0xfcd7e8fe47aa509afa0acdef86b741656c2602626f3258f8a83b359c665d6b04" } ], "ERC721Mint": [ @@ -34,10 +34,10 @@ ], "NFTGated": [ { - "address": "0xDf0ed657bdf9e8475D2Ec36F801Bf2cDaacd9188", - "blockNumber": 33511957, + "address": "0x85019550ff9b0109089391B7BE2653B95949Fc25", + "blockNumber": 33558820, "paramsSchema": "(address nft,uint8 tokenType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned", - "transactionHash": "0xaa9c235611578cc635e7bc072a8de65e99a56c391c5b76939604856957c59cd1" + "transactionHash": "0x6894af0785c5044c1c9a3735c97206bbde9a3002696b121d7fbff92cdaf978f2" } ] }, diff --git a/src/hooks/actions/ERC20Gated/ERC20Gated.sol b/src/hooks/actions/ERC20Gated/ERC20Gated.sol index bbc4d4e..13e09a5 100644 --- a/src/hooks/actions/ERC20Gated/ERC20Gated.sol +++ b/src/hooks/actions/ERC20Gated/ERC20Gated.sol @@ -64,6 +64,8 @@ contract ERC20Gated is RegistryOnchainAction { function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { (ERC20Gate[] memory gates) = abi.decode(params, (ERC20Gate[])); + delete tokenGates[slicerId][productId]; + for (uint256 i = 0; i < gates.length; i++) { tokenGates[slicerId][productId].push(gates[i]); } diff --git a/src/hooks/actions/ERC20Mint/ERC20Mint.sol b/src/hooks/actions/ERC20Mint/ERC20Mint.sol index cf17e0c..cde3489 100644 --- a/src/hooks/actions/ERC20Mint/ERC20Mint.sol +++ b/src/hooks/actions/ERC20Mint/ERC20Mint.sol @@ -123,6 +123,6 @@ contract ERC20Mint is RegistryOnchainAction { */ function paramsSchema() external pure override returns (string memory) { return - "string name,string symbol,uint256 premintAmount,address premintReceiver,uint256 maxSupply,uint256 tokensPerUnit"; + "string name,string symbol,uint256 premintAmount,address premintReceiver,bool revertOnMaxSupplyReached,uint256 maxSupply,uint256 tokensPerUnit"; } } diff --git a/src/hooks/actions/NFTGated/NFTGated.sol b/src/hooks/actions/NFTGated/NFTGated.sol index 15d195b..6d099b2 100644 --- a/src/hooks/actions/NFTGated/NFTGated.sol +++ b/src/hooks/actions/NFTGated/NFTGated.sol @@ -75,6 +75,8 @@ contract NFTGated is RegistryOnchainAction { function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { (NFTGates memory nftGates_) = abi.decode(params, (NFTGates)); + delete nftGates[slicerId][productId].gates; + nftGates[slicerId][productId].minOwned = nftGates_.minOwned; for (uint256 i = 0; i < nftGates_.gates.length; i++) { nftGates[slicerId][productId].gates.push(nftGates_.gates[i]); diff --git a/test/actions/ERC20Gated/ERC20Gated.t.sol b/test/actions/ERC20Gated/ERC20Gated.t.sol index 6746e4e..3c0b7b6 100644 --- a/test/actions/ERC20Gated/ERC20Gated.t.sol +++ b/test/actions/ERC20Gated/ERC20Gated.t.sol @@ -2,18 +2,20 @@ pragma solidity ^0.8.20; import {RegistryOnchainActionTest} from "@test/utils/RegistryOnchainActionTest.sol"; -import {ERC20Gated, ERC20Gate} from "@/hooks/actions/ERC20Gated/ERC20Gated.sol"; +import {MockERC20Gated} from "./mocks/MockERC20Gated.sol"; +import {ERC20Gate} from "@/hooks/actions/ERC20Gated/ERC20Gated.sol"; import {IERC20, MockERC20} from "@test/utils/mocks/MockERC20.sol"; uint256 constant slicerId = 0; uint256 constant productId = 1; contract ERC20GatedTest is RegistryOnchainActionTest { - ERC20Gated erc20Gated; + MockERC20Gated erc20Gated; MockERC20 token = new MockERC20(); + MockERC20 token2 = new MockERC20(); function setUp() public { - erc20Gated = new ERC20Gated(PRODUCTS_MODULE); + erc20Gated = new MockERC20Gated(PRODUCTS_MODULE); _setHook(address(erc20Gated)); } @@ -29,6 +31,32 @@ contract ERC20GatedTest is RegistryOnchainActionTest { assertTrue(amount == 100); } + function testReconfigureProduct() public { + ERC20Gate[] memory gates = new ERC20Gate[](2); + gates[0] = ERC20Gate(token, 100); + gates[1] = ERC20Gate(token2, 200); + + vm.startPrank(productOwner); + erc20Gated.configureProduct(slicerId, productId, 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); + + MockERC20 token3 = new MockERC20(); + 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); + + vm.stopPrank(); + } + function testIsPurchaseAllowed() public { ERC20Gate[] memory gates = new ERC20Gate[](1); gates[0] = ERC20Gate(token, 100); diff --git a/test/actions/ERC20Gated/mocks/MockERC20Gated.sol b/test/actions/ERC20Gated/mocks/MockERC20Gated.sol new file mode 100644 index 0000000..fb77b8f --- /dev/null +++ b/test/actions/ERC20Gated/mocks/MockERC20Gated.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IProductsModule, ERC20Gated, ERC20Gate} from "@/hooks/actions/ERC20Gated/ERC20Gated.sol"; + +contract MockERC20Gated is ERC20Gated { + constructor(IProductsModule productsModuleAddress) ERC20Gated(productsModuleAddress) {} + + function gates(uint256 slicerId, uint256 productId) public view returns (ERC20Gate[] memory) { + return tokenGates[slicerId][productId]; + } +} diff --git a/test/actions/NFTGated/NFTGated.t.sol b/test/actions/NFTGated/NFTGated.t.sol index 2a769af..7813bc9 100644 --- a/test/actions/NFTGated/NFTGated.t.sol +++ b/test/actions/NFTGated/NFTGated.t.sol @@ -2,7 +2,8 @@ pragma solidity ^0.8.20; import {RegistryOnchainActionTest} from "@test/utils/RegistryOnchainActionTest.sol"; -import {NFTGated, NFTGates, NFTGate, TokenType} from "@/hooks/actions/NFTGated/NFTGated.sol"; +import {MockNFTGated} from "./mocks/MockNFTGated.sol"; +import {NFTGates, NFTGate, TokenType} from "@/hooks/actions/NFTGated/NFTGated.sol"; import {MockERC721} from "@test/utils/mocks/MockERC721.sol"; import {MockERC1155} from "@test/utils/mocks/MockERC1155.sol"; @@ -11,14 +12,14 @@ import {console2} from "forge-std/console2.sol"; uint256 constant slicerId = 0; contract NFTGatedTest is RegistryOnchainActionTest { - NFTGated nftGated; + MockNFTGated nftGated; MockERC721 nft721 = new MockERC721(); MockERC1155 nft1155 = new MockERC1155(); uint256[] productIds = [1, 2, 3, 4]; function setUp() public { - nftGated = new NFTGated(PRODUCTS_MODULE); + nftGated = new MockNFTGated(PRODUCTS_MODULE); _setHook(address(nftGated)); } @@ -33,6 +34,23 @@ contract NFTGatedTest is RegistryOnchainActionTest { vm.stopPrank(); } + function testReconfigureProduct() public { + NFTGates[] memory nftGates = generateNFTGates(); + + 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], abi.encode(nftGates[1])); + assertEq(nftGated.gates(slicerId, productIds[2])[0].nft, address(nft1155)); + assertEq(nftGated.gates(slicerId, productIds[2]).length, 1); + + vm.stopPrank(); + } + function testIsPurchaseAllowed() public { NFTGates[] memory nftGates = generateNFTGates(); diff --git a/test/actions/NFTGated/mocks/MockNFTGated.sol b/test/actions/NFTGated/mocks/MockNFTGated.sol new file mode 100644 index 0000000..4f944c4 --- /dev/null +++ b/test/actions/NFTGated/mocks/MockNFTGated.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IProductsModule, NFTGated, NFTGate} from "@/hooks/actions/NFTGated/NFTGated.sol"; + +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; + } +} From 27d31721d46db46e9789a0ab2f144c6cccd49a6f Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Thu, 31 Jul 2025 18:54:35 +0200 Subject: [PATCH 07/24] wip --- .../TieredDiscount/NFTDiscount.t.sol | 305 +++++++----------- 1 file changed, 112 insertions(+), 193 deletions(-) diff --git a/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol b/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol index be445e8..357c1d8 100644 --- a/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol +++ b/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol @@ -6,10 +6,7 @@ import {console2} from "forge-std/console2.sol"; import { IProductsModule, NFTDiscount, - ProductDiscounts, - DiscountType, DiscountParams, - CurrencyParams, NFTType } from "@/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol"; import {MockERC721} from "@test/utils/mocks/MockERC721.sol"; @@ -19,9 +16,8 @@ address constant ETH = address(0); address constant USDC = address(1); uint256 constant slicerId = 0; uint256 constant productId = 1; -uint80 constant fixedDiscountOne = 100; -uint80 constant fixedDiscountTwo = 200; -uint80 constant percentDiscount = 1000; // 10% +uint80 constant percentDiscountOne = 1000; // 10% +uint80 constant percentDiscountTwo = 2000; // 20% contract NFTDiscountTest is RegistryPricingStrategyTest { NFTDiscount erc721GatedDiscount; @@ -30,7 +26,6 @@ contract NFTDiscountTest is RegistryPricingStrategyTest { MockERC721 nftThree = new MockERC721(); MockERC1155 nft1155 = new MockERC1155(); - uint240 basePrice = 1000; uint256 quantity = 1; uint8 minNftQuantity = 1; @@ -41,192 +36,175 @@ contract NFTDiscountTest is RegistryPricingStrategyTest { nftOne.mint(buyer); } - function createDiscount(DiscountParams[] memory discountParams) internal { - DiscountParams[] memory discounts = new DiscountParams[](discountParams.length); - - for (uint256 i = 0; i < discountParams.length; i++) { - discounts[i] = discountParams[i]; - } - - CurrencyParams[] memory currenciesParams = new CurrencyParams[](1); - currenciesParams[0] = CurrencyParams(ETH, basePrice, false, DiscountType.Absolute, discounts); - - vm.prank(productOwner); - erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(currenciesParams)); - } - function testConfigureProduct__ETH() public { - DiscountParams[] memory discounts = new DiscountParams[](1); + DiscountParams[] memory discountParams = new DiscountParams[](1); /// set product price with additional custom inputs - discounts[0] = DiscountParams({ + discountParams[0] = DiscountParams({ nft: address(nftOne), - discount: fixedDiscountOne, + discount: percentDiscountOne, minQuantity: minNftQuantity, nftType: NFTType.ERC721, tokenId: 0 }); - CurrencyParams[] memory currenciesParams = new CurrencyParams[](1); - currenciesParams[0] = CurrencyParams(ETH, basePrice, false, DiscountType.Absolute, discounts); - vm.prank(productOwner); - erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(currenciesParams)); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); /// check product price (uint256 ethPrice, uint256 currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - assertTrue(ethPrice == quantity * (basePrice - fixedDiscountOne)); - assertTrue(currencyPrice == 0); + (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + + assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); + assertEq(currencyPrice, 0); } function testConfigureProduct__ERC20() public { - DiscountParams[] memory discounts = new DiscountParams[](1); + DiscountParams[] memory discountParams = new DiscountParams[](1); /// set product price with additional custom inputs - discounts[0] = DiscountParams({ + discountParams[0] = DiscountParams({ nft: address(nftOne), - discount: fixedDiscountOne, + discount: percentDiscountOne, minQuantity: minNftQuantity, nftType: NFTType.ERC721, tokenId: 0 }); - CurrencyParams[] memory currenciesParams = new CurrencyParams[](1); - currenciesParams[0] = CurrencyParams(USDC, basePrice, false, DiscountType.Absolute, discounts); - vm.prank(productOwner); - erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(currenciesParams)); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); /// check product price (uint256 ethPrice, uint256 currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, USDC, quantity, buyer, ""); - assertTrue(currencyPrice == quantity * (basePrice - fixedDiscountOne)); + (, uint256 baseCurrencyPrice) = PRODUCTS_MODULE.basePrice(slicerId, productId, USDC, quantity); + + assertEq(currencyPrice, quantity * (baseCurrencyPrice - (baseCurrencyPrice * percentDiscountOne) / 1e4)); assertTrue(ethPrice == 0); } function testConfigureProduct__ERC1155() public { - DiscountParams[] memory discounts = new DiscountParams[](1); + DiscountParams[] memory discountParams = new DiscountParams[](1); /// set product price with additional custom inputs - discounts[0] = DiscountParams({ + discountParams[0] = DiscountParams({ nft: address(nft1155), - discount: fixedDiscountOne, + discount: percentDiscountOne, minQuantity: minNftQuantity, nftType: NFTType.ERC1155, tokenId: 1 }); - CurrencyParams[] memory currenciesParams = new CurrencyParams[](1); - currenciesParams[0] = CurrencyParams(USDC, basePrice, false, DiscountType.Absolute, discounts); - vm.prank(productOwner); - erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(currenciesParams)); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); /// check product price (uint256 ethPrice, uint256 currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, USDC, quantity, buyer, ""); - assertTrue(currencyPrice == quantity * basePrice); - assertTrue(ethPrice == 0); + (uint256 baseEthPrice, uint256 baseCurrencyPrice) = + PRODUCTS_MODULE.basePrice(slicerId, productId, USDC, quantity); + + assertEq(currencyPrice, quantity * baseCurrencyPrice); + assertEq(ethPrice, 0); nft1155.mint(buyer); (ethPrice, currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, USDC, quantity, buyer, ""); - assertTrue(currencyPrice == quantity * (basePrice - fixedDiscountOne)); - assertTrue(ethPrice == 0); + assertEq(currencyPrice, quantity * (baseCurrencyPrice - (baseCurrencyPrice * percentDiscountOne) / 1e4)); + assertEq(ethPrice, 0); } - function testConfigureProduct__MultipleCurrencies() public { - DiscountParams[] memory discountsOne = new DiscountParams[](1); - DiscountParams[] memory discountsTwo = new DiscountParams[](1); - CurrencyParams[] memory currenciesParams = new CurrencyParams[](2); + function testConfigureProduct__HigherDiscount() public { + DiscountParams[] memory discountParams = new DiscountParams[](2); - /// set product price with additional custom inputs - discountsOne[0] = DiscountParams({ - nft: address(nftOne), - discount: fixedDiscountOne, + discountParams[0] = DiscountParams({ + nft: address(nft1155), + discount: percentDiscountTwo, minQuantity: minNftQuantity, - nftType: NFTType.ERC721, - tokenId: 0 + nftType: NFTType.ERC1155, + tokenId: 1 }); - - currenciesParams[0] = CurrencyParams(ETH, basePrice, false, DiscountType.Absolute, discountsOne); - - /// set product price with different discount for different currency - discountsTwo[0] = DiscountParams({ + discountParams[1] = DiscountParams({ nft: address(nftOne), - discount: fixedDiscountTwo, + discount: percentDiscountOne, minQuantity: minNftQuantity, nftType: NFTType.ERC721, tokenId: 0 }); - currenciesParams[1] = CurrencyParams(USDC, basePrice, false, DiscountType.Absolute, discountsTwo); - vm.prank(productOwner); - erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(currenciesParams)); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); /// check product price for ETH (uint256 ethPrice, uint256 currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - assertTrue(ethPrice == quantity * (basePrice - fixedDiscountOne)); - assertTrue(currencyPrice == 0); + (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); - /// check product price for USDC - (uint256 ethPrice2, uint256 usdcPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, USDC, quantity, buyer, ""); + assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); + assertEq(currencyPrice, 0); + + nft1155.mint(buyer); + + (ethPrice, currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - assertTrue(ethPrice2 == 0); - assertTrue(usdcPrice == (quantity * basePrice) - fixedDiscountTwo); + assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountTwo) / 1e4)); + assertEq(currencyPrice, 0); } - function testProductPrice__NotNFTOwner() public { - DiscountParams[] memory discounts = new DiscountParams[](1); + function testRevert_ProductPrice__NotNFTOwner() public { + DiscountParams[] memory discountParams = new DiscountParams[](1); /// set product price for NFT that is not owned by buyer - discounts[0] = DiscountParams({ + discountParams[0] = DiscountParams({ nft: address(nftTwo), - discount: fixedDiscountOne, + discount: percentDiscountOne, minQuantity: minNftQuantity, nftType: NFTType.ERC721, tokenId: 0 }); - createDiscount(discounts); + vm.prank(productOwner); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); /// check product price (uint256 ethPrice, uint256 currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - assertTrue(ethPrice == quantity * basePrice); - assertTrue(currencyPrice == 0); + (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + + assertEq(ethPrice, quantity * baseEthPrice); + assertEq(currencyPrice, 0); } function testProductPrice__MinQuantity() public { - DiscountParams[] memory discounts = new DiscountParams[](1); + DiscountParams[] memory discountParams = new DiscountParams[](1); /// Buyer owns 1 NFT, but minQuantity is 2 - discounts[0] = DiscountParams({ + discountParams[0] = DiscountParams({ nft: address(nftOne), - discount: fixedDiscountOne, + discount: percentDiscountOne, minQuantity: minNftQuantity + 1, nftType: NFTType.ERC721, tokenId: 0 }); - createDiscount(discounts); + vm.prank(productOwner); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); /// check product price (uint256 ethPrice, uint256 currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - assertTrue(ethPrice == quantity * basePrice); - assertTrue(currencyPrice == 0); + (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + + assertEq(ethPrice, quantity * baseEthPrice); + assertEq(currencyPrice, 0); /// Buyer owns 2 NFTs, minQuantity is 2 nftOne.mint(buyer); @@ -235,92 +213,23 @@ contract NFTDiscountTest is RegistryPricingStrategyTest { (uint256 secondEthPrice, uint256 secondCurrencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - assertTrue(secondEthPrice == quantity * (basePrice - fixedDiscountOne)); - assertTrue(secondCurrencyPrice == 0); - } - - function testProductPrice__HigherDiscount() public { - DiscountParams[] memory discounts = new DiscountParams[](2); - - /// NFT 2 has higher discount, but buyer owns only NFT 1 - discounts[0] = DiscountParams({ - nft: address(nftTwo), - discount: fixedDiscountTwo, - minQuantity: minNftQuantity, - nftType: NFTType.ERC721, - tokenId: 0 - }); - discounts[1] = DiscountParams({ - nft: address(nftOne), - discount: fixedDiscountOne, - minQuantity: minNftQuantity, - nftType: NFTType.ERC721, - tokenId: 0 - }); - - createDiscount(discounts); - - /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - - assertTrue(ethPrice == quantity * (basePrice - fixedDiscountOne)); - assertTrue(currencyPrice == 0); - - /// Buyer mints NFT 2 - nftTwo.mint(buyer); - - /// check product price - (uint256 secondEthPrice, uint256 secondCurrencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - - assertTrue(secondEthPrice == quantity * (basePrice - fixedDiscountTwo)); - assertTrue(secondCurrencyPrice == 0); - } - - function testProductPrice__Relative() public { - DiscountParams[] memory discounts = new DiscountParams[](1); - - discounts[0] = DiscountParams({ - nft: address(nftOne), - discount: percentDiscount, - minQuantity: minNftQuantity, - nftType: NFTType.ERC721, - tokenId: 0 - }); - - CurrencyParams[] memory currenciesParams = new CurrencyParams[](1); - /// set product price with percentage discount - currenciesParams[0] = CurrencyParams(ETH, basePrice, false, DiscountType.Relative, discounts); - - vm.prank(productOwner); - erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(currenciesParams)); - - /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - - assertTrue(ethPrice == quantity * (basePrice - (basePrice * percentDiscount) / 1e4)); - assertTrue(currencyPrice == 0); + assertEq(secondEthPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); + assertEq(secondCurrencyPrice, 0); } function testProductPrice__MultipleBoughtQuantity() public { - DiscountParams[] memory discounts = new DiscountParams[](1); + DiscountParams[] memory discountParams = new DiscountParams[](1); - discounts[0] = DiscountParams({ + discountParams[0] = DiscountParams({ nft: address(nftOne), - discount: percentDiscount, + discount: percentDiscountOne, minQuantity: minNftQuantity, nftType: NFTType.ERC721, tokenId: 0 }); - CurrencyParams[] memory currenciesParams = new CurrencyParams[](1); - /// set product price with percentage discount - currenciesParams[0] = CurrencyParams(ETH, basePrice, false, DiscountType.Relative, discounts); - vm.prank(productOwner); - erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(currenciesParams)); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); // buy multiple products quantity = 6; @@ -329,22 +238,25 @@ contract NFTDiscountTest is RegistryPricingStrategyTest { (uint256 ethPrice, uint256 currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - assertTrue(ethPrice == quantity * (basePrice - (basePrice * percentDiscount) / 1e4)); - assertTrue(currencyPrice == 0); + (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + + assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); + assertEq(currencyPrice, 0); } function testConfigureProduct__Edit_Add() public { - DiscountParams[] memory discounts = new DiscountParams[](1); + DiscountParams[] memory discountParams = new DiscountParams[](1); - discounts[0] = DiscountParams({ + discountParams[0] = DiscountParams({ nft: address(nftTwo), - discount: fixedDiscountTwo, + discount: percentDiscountTwo, minQuantity: minNftQuantity, nftType: NFTType.ERC721, tokenId: 0 }); - createDiscount(discounts); + vm.prank(productOwner); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); // mint NFT 2 nftTwo.mint(buyer); @@ -353,87 +265,94 @@ contract NFTDiscountTest is RegistryPricingStrategyTest { (uint256 ethPrice, uint256 currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - assertTrue(ethPrice == quantity * (basePrice - fixedDiscountTwo)); - assertTrue(currencyPrice == 0); + (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); - discounts = new DiscountParams[](2); + assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountTwo) / 1e4)); + assertEq(currencyPrice, 0); + + discountParams = new DiscountParams[](2); /// edit product price, with more NFTs and first NFT has higher discount but buyer owns only the second - discounts[0] = DiscountParams({ + discountParams[0] = DiscountParams({ nft: address(nftThree), - discount: fixedDiscountOne + 10, + discount: percentDiscountOne + 10, minQuantity: minNftQuantity, nftType: NFTType.ERC721, tokenId: 0 }); - discounts[1] = DiscountParams({ + discountParams[1] = DiscountParams({ nft: address(nftOne), - discount: fixedDiscountOne, + discount: percentDiscountOne, minQuantity: minNftQuantity, nftType: NFTType.ERC721, tokenId: 0 }); - createDiscount(discounts); + vm.prank(productOwner); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); /// check product price (uint256 secondEthPrice, uint256 secondCurrencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - assertTrue(secondEthPrice == quantity * (basePrice - fixedDiscountOne)); - assertTrue(secondCurrencyPrice == 0); + assertEq(secondEthPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); + assertEq(secondCurrencyPrice, 0); } function testConfigureProduct__Edit_Remove() public { - DiscountParams[] memory discounts = new DiscountParams[](2); + DiscountParams[] memory discountParams = new DiscountParams[](2); // mint NFT 2 nftTwo.mint(buyer); /// edit product price, with more NFTs and first NFT has higher discount but buyer owns only the second - discounts[0] = DiscountParams({ + discountParams[0] = DiscountParams({ nft: address(nftThree), - discount: fixedDiscountOne + 10, + discount: percentDiscountOne + 10, minQuantity: minNftQuantity, nftType: NFTType.ERC721, tokenId: 0 }); - discounts[1] = DiscountParams({ + discountParams[1] = DiscountParams({ nft: address(nftOne), - discount: fixedDiscountOne, + discount: percentDiscountOne, minQuantity: minNftQuantity, nftType: NFTType.ERC721, tokenId: 0 }); - createDiscount(discounts); + vm.prank(productOwner); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); /// check product price (uint256 secondEthPrice, uint256 secondCurrencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - assertTrue(secondEthPrice == quantity * (basePrice - fixedDiscountOne)); - assertTrue(secondCurrencyPrice == 0); + (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); - discounts = new DiscountParams[](1); + assertEq(secondEthPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); + assertEq(secondCurrencyPrice, 0); - discounts[0] = DiscountParams({ + discountParams = new DiscountParams[](1); + + discountParams[0] = DiscountParams({ nft: address(nftTwo), - discount: fixedDiscountTwo, + discount: percentDiscountTwo, minQuantity: minNftQuantity, nftType: NFTType.ERC721, tokenId: 0 }); - createDiscount(discounts); + vm.prank(productOwner); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); /// check product price (uint256 ethPrice, uint256 currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - assertTrue(ethPrice == quantity * (basePrice - fixedDiscountTwo)); - assertTrue(currencyPrice == 0); + assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountTwo) / 1e4)); + assertEq(currencyPrice, 0); } } From fff2ee61b1026e2ecefd735f024360030953320f Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Thu, 31 Jul 2025 18:57:46 +0200 Subject: [PATCH 08/24] update nft hooks --- deployments/addresses.json | 24 +++--- script/setupContractTest.sh | 16 ---- src/hooks/actions/NFTGated/NFTGated.sol | 6 +- src/hooks/actions/NFTGated/types/NFTGate.sol | 4 +- .../NFTDiscount/NFTDiscount.sol | 86 +++++++------------ .../pricing/TieredDiscount/TieredDiscount.sol | 29 ++++--- .../TieredDiscount/types/CurrencyParams.sol | 18 ---- .../TieredDiscount/types/ProductDiscounts.sol | 20 ----- test/actions/NFTGated/NFTGated.t.sol | 6 +- .../TieredDiscount/NFTDiscount.t.sol | 3 +- test/utils/mocks/MockProductsModule.sol | 9 ++ 11 files changed, 78 insertions(+), 143 deletions(-) delete mode 100755 script/setupContractTest.sh delete mode 100644 src/hooks/pricing/TieredDiscount/types/CurrencyParams.sol delete mode 100644 src/hooks/pricing/TieredDiscount/types/ProductDiscounts.sol diff --git a/deployments/addresses.json b/deployments/addresses.json index d2eb4fa..9a03f26 100644 --- a/deployments/addresses.json +++ b/deployments/addresses.json @@ -34,22 +34,14 @@ ], "NFTGated": [ { - "address": "0x85019550ff9b0109089391B7BE2653B95949Fc25", - "blockNumber": 33558820, - "paramsSchema": "(address nft,uint8 tokenType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned", - "transactionHash": "0x6894af0785c5044c1c9a3735c97206bbde9a3002696b121d7fbff92cdaf978f2" + "address": "0xD4eF7A46bF4c58036eaCA886119F5230e5a2C25d", + "blockNumber": 33563508, + "paramsSchema": "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned", + "transactionHash": "0x9dd101a6155b432849cb33f65c5d4b9f6875e6b1e7bf806005a8a6b0d070f634" } ] }, "pricing": { - "NFTDiscount": [ - { - "address": "0x2Bc8DF056B6962C8A534aeD9eeBC7d21011E1E71", - "blockNumber": 33512225, - "paramsSchema": "(address currency,uint240 basePrice,bool isFree,uint8 discountType,(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[]discounts)[] allCurrencyParams", - "transactionHash": "0xe45ebfd62cc49d6a0c19e232e6351e8c1209fb64ea079524cd8d8a848b5a1c4b" - } - ], "LinearVRGDAPrices": [ { "address": "0xEC68E30182F4298b7032400B7ce809da613e4449", @@ -65,6 +57,14 @@ "paramsSchema": "(address currency,int128 targetPrice,uint128 min,int256 timeScale)[] logisticParams,int256 priceDecayPercent", "transactionHash": "0x2b27380661a38b54dbccf634305622296034ddfccf5865a07fbfb7810ea41025" } + ], + "NFTDiscount": [ + { + "address": "0xffdC32a35a7ADE286e8Ff11B93aeAFf48c659D4d", + "blockNumber": 33595797, + "paramsSchema": "(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[] discounts", + "transactionHash": "0x461a6c45cebbfb33690bd4bb1d69ec7775a82d1706a58463b0fe17b735e9c79a" + } ] }, "pricingActions": { diff --git a/script/setupContractTest.sh b/script/setupContractTest.sh deleted file mode 100755 index 71f9d6e..0000000 --- a/script/setupContractTest.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -source .env - -contractName="$1" - -if [ -z "$contractName" ]; then - output=$(forge script script/Deploy.s.sol --chain base --rpc-url base --private-key $PRIVATE_KEY --verify -vvvv --broadcast --slow) - - contractName=$(echo "$output" | grep 'contractName:' | awk -F'"' '{print $2}') -else - forge script script/Deploy.s.sol --chain base --rpc-url base --private-key $PRIVATE_KEY --sig "run(string memory contractName)" "$contractName" --verify -vvvv --broadcast --slow -fi - -forge script script/WriteAddresses.s.sol --sig "run(string memory contractName)" "$contractName" - -echo "Deployed contract: $contractName" \ No newline at end of file diff --git a/src/hooks/actions/NFTGated/NFTGated.sol b/src/hooks/actions/NFTGated/NFTGated.sol index 6d099b2..ab3c614 100644 --- a/src/hooks/actions/NFTGated/NFTGated.sol +++ b/src/hooks/actions/NFTGated/NFTGated.sol @@ -10,7 +10,7 @@ import { IOnchainAction, IHookRegistry } from "@/utils/RegistryOnchainAction.sol"; -import {TokenType, NFTGate, NFTGates} from "./types/NFTGate.sol"; +import {NftType, NFTGate, NFTGates} from "./types/NFTGate.sol"; /** * @title NFTGated @@ -53,7 +53,7 @@ contract NFTGated is RegistryOnchainAction { for (uint256 i; i < nftGates_.gates.length;) { NFTGate memory gate = nftGates_.gates[i]; - if (gate.tokenType == TokenType.ERC1155) { + if (gate.nftType == NftType.ERC1155) { if (IERC1155(gate.nft).balanceOf(account, gate.id) >= gate.minQuantity) { ++totalOwned; } @@ -87,6 +87,6 @@ contract NFTGated is RegistryOnchainAction { * @inheritdoc IHookRegistry */ function paramsSchema() external pure override returns (string memory) { - return "(address nft,uint8 tokenType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned"; + return "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned"; } } diff --git a/src/hooks/actions/NFTGated/types/NFTGate.sol b/src/hooks/actions/NFTGated/types/NFTGate.sol index ff2a95c..bb54a52 100644 --- a/src/hooks/actions/NFTGated/types/NFTGate.sol +++ b/src/hooks/actions/NFTGated/types/NFTGate.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -enum TokenType { +enum NftType { ERC721, ERC1155 } struct NFTGate { address nft; - TokenType tokenType; + NftType nftType; uint80 id; uint8 minQuantity; } diff --git a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol index f2b1e3b..2d39d9c 100644 --- a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.20; 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/RegistryPricingStrategy.sol"; -import {DiscountParams, ProductDiscounts, DiscountType, TieredDiscount, NFTType} from "../TieredDiscount.sol"; -import {CurrencyParams} from "../types/CurrencyParams.sol"; +import {DiscountParams, TieredDiscount} from "../TieredDiscount.sol"; +import {NFTType} from "../types/DiscountParams.sol"; /** * @title NFTDiscount @@ -26,31 +26,21 @@ contract NFTDiscount is TieredDiscount { /** * @inheritdoc HookRegistry * @notice Set base price and NFT discounts for a product. - * @dev Discounts must be sorted in descending order + * @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 { - (CurrencyParams[] memory allCurrencyParams) = abi.decode(params, (CurrencyParams[])); + (DiscountParams[] memory newDiscounts) = abi.decode(params, (DiscountParams[])); + + DiscountParams[] storage productDiscount = discounts[slicerId][productId]; - CurrencyParams memory currencyParams; - DiscountParams[] memory newDiscounts; uint256 prevDiscountValue; uint256 prevDiscountsLength; uint256 currDiscountsLength; uint256 maxLength; uint256 minLength; - for (uint256 i; i < allCurrencyParams.length;) { - currencyParams = allCurrencyParams[i]; - - ProductDiscounts storage productDiscount = productDiscounts[slicerId][productId][currencyParams.currency]; - - // Set `productDiscount` values - productDiscount.basePrice = currencyParams.basePrice; - productDiscount.isFree = currencyParams.isFree; - productDiscount.discountType = currencyParams.discountType; - + for (uint256 i; i < newDiscounts.length;) { // Set values used in inner loop - newDiscounts = currencyParams.discounts; - prevDiscountsLength = productDiscount.discountsArray.length; + prevDiscountsLength = productDiscount.length; currDiscountsLength = newDiscounts.length; maxLength = currDiscountsLength > prevDiscountsLength ? currDiscountsLength : prevDiscountsLength; minLength = maxLength == prevDiscountsLength ? currDiscountsLength : prevDiscountsLength; @@ -58,11 +48,9 @@ contract NFTDiscount is TieredDiscount { for (uint256 j; j < maxLength;) { // If `j` is within bounds of `newDiscounts` if (currDiscountsLength > j) { - // Check relative discount doesn't exceed max value of 1e4 - if (currencyParams.discountType == DiscountType.Relative) { - if (newDiscounts[j].discount > 1e4) { - revert InvalidRelativeDiscount(); - } + // Check relative discount doesn't exceed max value of 1e4 (100%) + if (newDiscounts[j].discount > 1e4) { + revert InvalidRelativeAmount(); } if (newDiscounts[j].minQuantity == 0) { @@ -80,14 +68,14 @@ contract NFTDiscount is TieredDiscount { if (j < minLength) { // Update in place - productDiscount.discountsArray[j] = newDiscounts[j]; + productDiscount[j] = newDiscounts[j]; } else if (j >= prevDiscountsLength) { // Append new discounts - productDiscount.discountsArray.push(newDiscounts[j]); + productDiscount.push(newDiscounts[j]); } } else { // Remove old discounts - productDiscount.discountsArray.pop(); + productDiscount.pop(); } unchecked { @@ -105,8 +93,7 @@ contract NFTDiscount is TieredDiscount { * @inheritdoc IHookRegistry */ function paramsSchema() external pure override returns (string memory) { - return - "(address currency,uint240 basePrice,bool isFree,uint8 discountType,(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[]discounts)[] allCurrencyParams"; + return "(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[] discounts"; } /** @@ -120,13 +107,12 @@ contract NFTDiscount is TieredDiscount { uint256 quantity, address buyer, bytes memory, - ProductDiscounts memory discountParams + uint256 basePrice, + DiscountParams[] memory discountParams ) internal view virtual override returns (uint256 ethPrice, uint256 currencyPrice) { uint256 discount = _getHighestDiscount(discountParams, buyer); - uint256 price = discount != 0 - ? _getPriceBasedOnDiscountType(discountParams.basePrice, discountParams.discountType, discount, quantity) - : quantity * discountParams.basePrice; + uint256 price = discount != 0 ? _getDiscountedPrice(basePrice, discount, quantity) : quantity * basePrice; if (currency == address(0)) { ethPrice = price; @@ -147,21 +133,20 @@ contract NFTDiscount is TieredDiscount { * * @return Discount value */ - function _getHighestDiscount(ProductDiscounts memory discountParams, address buyer) + function _getHighestDiscount(DiscountParams[] memory discountParams, address buyer) internal view virtual returns (uint256) { - DiscountParams[] memory discounts = discountParams.discountsArray; - uint256 length = discounts.length; + uint256 length = discountParams.length; DiscountParams memory el; address prevAsset; uint256 prevTokenId; uint256 nftBalance; for (uint256 i; i < length;) { - el = discounts[i]; + el = discountParams[i]; // Skip retrieving balance if asset is the same as previous iteration if (el.nftType == NFTType.ERC1155) { @@ -195,28 +180,23 @@ contract NFTDiscount is TieredDiscount { * @notice Calculate price based on `discountType` * * @param basePrice Base price of the product - * @param discountType Type of discount, either `Absolute` or `Relative` * @param discount Discount value based on `discountType` * @param quantity Number of units purchased * * @return price of product inclusive of discount. */ - function _getPriceBasedOnDiscountType( - uint256 basePrice, - DiscountType discountType, - uint256 discount, - uint256 quantity - ) internal pure virtual returns (uint256 price) { - if (discountType == DiscountType.Absolute) { - price = (basePrice - discount) * quantity; - } else { - uint256 k; - /// @dev discount cannot be higher than 1e4, as it's checked on `setProductPrice` - unchecked { - k = 1e4 - discount; - } - - price = (basePrice * k * quantity) / 1e4; + function _getDiscountedPrice(uint256 basePrice, uint256 discount, uint256 quantity) + internal + pure + virtual + returns (uint256 price) + { + uint256 k; + /// @dev discount cannot be higher than 1e4, as it's checked on `setProductPrice` + unchecked { + k = 1e4 - discount; } + + price = (basePrice * k * quantity) / 1e4; } } diff --git a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol index 7b20829..8d2a3d8 100644 --- a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {RegistryPricingStrategy, IPricingStrategy, IProductsModule} from "@/utils/RegistryPricingStrategy.sol"; -import {ProductDiscounts, DiscountType} from "./types/ProductDiscounts.sol"; -import {DiscountParams, NFTType} from "./types/DiscountParams.sol"; +import {IProductsModule} from "slice/interfaces/IProductsModule.sol"; +import {RegistryPricingStrategy, IPricingStrategy} from "@/utils/RegistryPricingStrategy.sol"; +import {DiscountParams} from "./types/DiscountParams.sol"; /** * @title TieredDiscount @@ -16,7 +16,7 @@ abstract contract TieredDiscount is RegistryPricingStrategy { //////////////////////////////////////////////////////////////*/ error WrongCurrency(); - error InvalidRelativeDiscount(); + error InvalidRelativeAmount(); error InvalidMinQuantity(); error DiscountsNotDescending(DiscountParams nft); @@ -24,8 +24,7 @@ abstract contract TieredDiscount is RegistryPricingStrategy { MUTABLE STORAGE //////////////////////////////////////////////////////////////*/ - mapping(uint256 slicerId => mapping(uint256 productId => mapping(address currency => ProductDiscounts))) public - productDiscounts; + mapping(uint256 slicerId => mapping(uint256 productId => DiscountParams[])) public discounts; /*////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -48,13 +47,13 @@ abstract contract TieredDiscount is RegistryPricingStrategy { address buyer, bytes memory data ) public view override returns (uint256 ethPrice, uint256 currencyPrice) { - ProductDiscounts memory discountParams = productDiscounts[slicerId][productId][currency]; + (uint256 basePriceEth, uint256 basePriceCurrency) = + PRODUCTS_MODULE.basePrice(slicerId, productId, currency, quantity); + uint256 basePrice = currency == address(0) ? basePriceEth : basePriceCurrency; - if (discountParams.basePrice == 0) { - if (!discountParams.isFree) revert WrongCurrency(); - } else { - return _productPrice(slicerId, productId, currency, quantity, buyer, data, discountParams); - } + DiscountParams[] memory discountParams = discounts[slicerId][productId]; + + return _productPrice(slicerId, productId, currency, quantity, buyer, data, basePrice, discountParams); } /*////////////////////////////////////////////////////////////// @@ -70,7 +69,8 @@ abstract contract TieredDiscount is RegistryPricingStrategy { * @param quantity Number of units purchased * @param buyer Address of the buyer. * @param data Data passed to the productPrice function. - * @param discountParams `ProductDiscounts` struct. + * @param basePrice Base price of the product. + * @param discountParams Array of discount parameters. * * @return ethPrice and currencyPrice of product. */ @@ -81,6 +81,7 @@ abstract contract TieredDiscount is RegistryPricingStrategy { uint256 quantity, address buyer, bytes memory data, - ProductDiscounts memory discountParams + uint256 basePrice, + DiscountParams[] memory discountParams ) internal view virtual returns (uint256 ethPrice, uint256 currencyPrice); } diff --git a/src/hooks/pricing/TieredDiscount/types/CurrencyParams.sol b/src/hooks/pricing/TieredDiscount/types/CurrencyParams.sol deleted file mode 100644 index afe5d51..0000000 --- a/src/hooks/pricing/TieredDiscount/types/CurrencyParams.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {DiscountParams} from "./DiscountParams.sol"; -import {DiscountType} from "./ProductDiscounts.sol"; - -/// @param currency currency address -/// @param basePrice base price for a currency -/// @param isFree boolean flag that allows purchase when basePrice == 0` -/// @param discountType type of discount, can be `Absolute` or `Relative` -/// @param discounts array of DiscountParams -struct CurrencyParams { - address currency; - uint240 basePrice; - bool isFree; - DiscountType discountType; - DiscountParams[] discounts; -} diff --git a/src/hooks/pricing/TieredDiscount/types/ProductDiscounts.sol b/src/hooks/pricing/TieredDiscount/types/ProductDiscounts.sol deleted file mode 100644 index 497a8a2..0000000 --- a/src/hooks/pricing/TieredDiscount/types/ProductDiscounts.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {DiscountParams} from "./DiscountParams.sol"; - -enum DiscountType { - Absolute, - Relative -} - -/// @param basePrice base price for a currency -/// @param isFree boolean flag that allows purchase when basePrice == 0` -/// @param discountType type of discount, can be `Absolute` or `Relative` -/// @param nftDiscounts array of structs {asset address, absolute/relative discount, min quantity} -struct ProductDiscounts { - uint240 basePrice; - bool isFree; - DiscountType discountType; - DiscountParams[] discountsArray; -} diff --git a/test/actions/NFTGated/NFTGated.t.sol b/test/actions/NFTGated/NFTGated.t.sol index 7813bc9..e771678 100644 --- a/test/actions/NFTGated/NFTGated.t.sol +++ b/test/actions/NFTGated/NFTGated.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {RegistryOnchainActionTest} from "@test/utils/RegistryOnchainActionTest.sol"; import {MockNFTGated} from "./mocks/MockNFTGated.sol"; -import {NFTGates, NFTGate, TokenType} from "@/hooks/actions/NFTGated/NFTGated.sol"; +import {NFTGates, NFTGate, NftType} from "@/hooks/actions/NFTGated/NFTGated.sol"; import {MockERC721} from "@test/utils/mocks/MockERC721.sol"; import {MockERC1155} from "@test/utils/mocks/MockERC1155.sol"; @@ -92,8 +92,8 @@ contract NFTGatedTest is RegistryOnchainActionTest { function generateNFTGates() public view returns (NFTGates[] memory nftGates) { nftGates = new NFTGates[](4); - NFTGate memory gate721 = NFTGate(address(nft721), TokenType.ERC721, 1, 1); - NFTGate memory gate1155 = NFTGate(address(nft1155), TokenType.ERC1155, 1, 1); + NFTGate memory gate721 = NFTGate(address(nft721), NftType.ERC721, 1, 1); + NFTGate memory gate1155 = NFTGate(address(nft1155), NftType.ERC1155, 1, 1); // Only 721 is required NFTGate[] memory gates1 = new NFTGate[](1); diff --git a/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol b/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol index 357c1d8..a7e679e 100644 --- a/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol +++ b/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol @@ -105,8 +105,7 @@ contract NFTDiscountTest is RegistryPricingStrategyTest { (uint256 ethPrice, uint256 currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, USDC, quantity, buyer, ""); - (uint256 baseEthPrice, uint256 baseCurrencyPrice) = - PRODUCTS_MODULE.basePrice(slicerId, productId, USDC, quantity); + (, uint256 baseCurrencyPrice) = PRODUCTS_MODULE.basePrice(slicerId, productId, USDC, quantity); assertEq(currencyPrice, quantity * baseCurrencyPrice); assertEq(ethPrice, 0); diff --git a/test/utils/mocks/MockProductsModule.sol b/test/utils/mocks/MockProductsModule.sol index 335f87a..424348c 100644 --- a/test/utils/mocks/MockProductsModule.sol +++ b/test/utils/mocks/MockProductsModule.sol @@ -11,6 +11,15 @@ 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 availableUnits(uint256, uint256) external pure returns (uint256 units, bool isInfinite) { units = 6392; isInfinite = false; From 3ea383dc5517d66e0e817ece3d1e6d239784392f Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Thu, 31 Jul 2025 19:06:26 +0200 Subject: [PATCH 09/24] update deps --- foundry.toml | 4 ++-- soldeer.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/foundry.toml b/foundry.toml index 0eeb8bc..fc92a13 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,7 +7,7 @@ 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"}] remappings = [ - "slice/=dependencies/slice-0.0.4/", + "slice/=dependencies/slice-0.0.5/", "@openzeppelin-4.8.0/=dependencies/@openzeppelin-contracts-4.8.0/", "@erc721a/=dependencies/erc721a-4.3.0/contracts/", "@murky/=dependencies/murky-0.1.0/src/", @@ -44,7 +44,7 @@ remappings_generate = false remappings_regenerate = false [dependencies] -slice = "0.0.4" +slice = "0.0.5" forge-std = "1.9.7" "@openzeppelin-contracts" = "4.8.0" erc721a = "4.3.0" diff --git a/soldeer.lock b/soldeer.lock index 3f903d8..2c44d27 100644 --- a/soldeer.lock +++ b/soldeer.lock @@ -28,7 +28,7 @@ integrity = "a41bd6903adfe80291f7b20c0317368e517db10c302e82aa7dc53776f17811cd" [[dependencies]] name = "slice" -version = "0.0.4" -url = "https://soldeer-revisions.s3.amazonaws.com/slice/0_0_4_14-07-2025_00:59:56_src.zip" -checksum = "b71b5ab29290b4683269547cbe571044f7938e8940a3996be6da882534c29f9e" -integrity = "07e086edca2dff96ddc168abeea5e11d3d7aa0f0a964163d3de4c0d1b7acbb0c" +version = "0.0.5" +url = "https://soldeer-revisions.s3.amazonaws.com/slice/0_0_5_31-07-2025_17:05:17_src.zip" +checksum = "c27b2ae030af9a14ab8e443052f0e1f67076fdc314377a09c89293dc9f3d99ce" +integrity = "bdb01a680685add0d3e23470b672638222d0073a9241beeafe30de692ab12f28" From 605bebc4d0a3a55b28f1a3ceff983e4646d8c1bd Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Thu, 31 Jul 2025 20:03:23 +0200 Subject: [PATCH 10/24] update NFTDiscount --- deployments/addresses.json | 16 ++--- .../NFTDiscount/NFTDiscount.sol | 64 ++++++------------- .../TieredDiscount/NFTDiscount.t.sol | 12 +--- 3 files changed, 30 insertions(+), 62 deletions(-) diff --git a/deployments/addresses.json b/deployments/addresses.json index 9a03f26..c3a44b1 100644 --- a/deployments/addresses.json +++ b/deployments/addresses.json @@ -42,6 +42,14 @@ ] }, "pricing": { + "NFTDiscount": [ + { + "address": "0xb830a457d2f51d4cA1136b97FB30DF6366CFe2f5", + "blockNumber": 33596708, + "paramsSchema": "(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[] discounts", + "transactionHash": "0x966be5fa1da3fab7c5027c9acb3f118307997e30687188b4745d74fd26ad9e7e" + } + ], "LinearVRGDAPrices": [ { "address": "0xEC68E30182F4298b7032400B7ce809da613e4449", @@ -57,14 +65,6 @@ "paramsSchema": "(address currency,int128 targetPrice,uint128 min,int256 timeScale)[] logisticParams,int256 priceDecayPercent", "transactionHash": "0x2b27380661a38b54dbccf634305622296034ddfccf5865a07fbfb7810ea41025" } - ], - "NFTDiscount": [ - { - "address": "0xffdC32a35a7ADE286e8Ff11B93aeAFf48c659D4d", - "blockNumber": 33595797, - "paramsSchema": "(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[] discounts", - "transactionHash": "0x461a6c45cebbfb33690bd4bb1d69ec7775a82d1706a58463b0fe17b735e9c79a" - } ] }, "pricingActions": { diff --git a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol index 2d39d9c..2e7edfe 100644 --- a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol @@ -33,55 +33,31 @@ contract NFTDiscount is TieredDiscount { DiscountParams[] storage productDiscount = discounts[slicerId][productId]; + delete discounts[slicerId][productId]; + uint256 prevDiscountValue; - uint256 prevDiscountsLength; - uint256 currDiscountsLength; - uint256 maxLength; - uint256 minLength; + DiscountParams memory discountParam; for (uint256 i; i < newDiscounts.length;) { - // Set values used in inner loop - prevDiscountsLength = productDiscount.length; - currDiscountsLength = newDiscounts.length; - maxLength = currDiscountsLength > prevDiscountsLength ? currDiscountsLength : prevDiscountsLength; - minLength = maxLength == prevDiscountsLength ? currDiscountsLength : prevDiscountsLength; - - for (uint256 j; j < maxLength;) { - // If `j` is within bounds of `newDiscounts` - if (currDiscountsLength > j) { - // Check relative discount doesn't exceed max value of 1e4 (100%) - if (newDiscounts[j].discount > 1e4) { - revert InvalidRelativeAmount(); - } - - if (newDiscounts[j].minQuantity == 0) { - revert InvalidMinQuantity(); - } - - // Check discounts are sorted in descending order - if (j > 0) { - if (newDiscounts[j].discount > prevDiscountValue) { - revert DiscountsNotDescending(newDiscounts[j]); - } - } - - prevDiscountValue = newDiscounts[j].discount; - - if (j < minLength) { - // Update in place - productDiscount[j] = newDiscounts[j]; - } else if (j >= prevDiscountsLength) { - // Append new discounts - productDiscount.push(newDiscounts[j]); - } - } else { - // Remove old discounts - productDiscount.pop(); - } + discountParam = newDiscounts[i]; + + // Check relative discount doesn't exceed max value of 1e4 (100%) + if (discountParam.discount > 1e4) { + revert InvalidRelativeAmount(); + } - unchecked { - ++j; + 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; + + productDiscount.push(discountParam); unchecked { ++i; diff --git a/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol b/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol index a7e679e..8291c78 100644 --- a/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol +++ b/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol @@ -334,15 +334,7 @@ contract NFTDiscountTest is RegistryPricingStrategyTest { assertEq(secondEthPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); assertEq(secondCurrencyPrice, 0); - discountParams = new DiscountParams[](1); - - discountParams[0] = DiscountParams({ - nft: address(nftTwo), - discount: percentDiscountTwo, - minQuantity: minNftQuantity, - nftType: NFTType.ERC721, - tokenId: 0 - }); + discountParams = new DiscountParams[](0); vm.prank(productOwner); erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); @@ -351,7 +343,7 @@ contract NFTDiscountTest is RegistryPricingStrategyTest { (uint256 ethPrice, uint256 currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountTwo) / 1e4)); + assertEq(ethPrice, quantity * baseEthPrice); assertEq(currencyPrice, 0); } } From fb20bcad0d581d0a4be552ec08a553139e4e0efa Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Tue, 5 Aug 2025 17:51:03 +0200 Subject: [PATCH 11/24] hooks: update deps --- foundry.toml | 2 ++ soldeer.lock | 7 +++++++ test/actions/ERC20Gated/ERC20Gated.t.sol | 6 +++--- test/utils/mocks/MockERC20.sol | 6 ++++-- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/foundry.toml b/foundry.toml index fc92a13..8a00afc 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,6 +9,7 @@ fs_permissions = [{ access = "read", path = "./src"}, { access= "read", path = " remappings = [ "slice/=dependencies/slice-0.0.5/", "@openzeppelin-4.8.0/=dependencies/@openzeppelin-contracts-4.8.0/", + "@openzeppelin-upgradeable-4.8.0/=dependencies/@openzeppelin-contracts-upgradeable-4.8.0/", "@erc721a/=dependencies/erc721a-4.3.0/contracts/", "@murky/=dependencies/murky-0.1.0/src/", "forge-std/=dependencies/forge-std-1.9.7/src/", @@ -47,6 +48,7 @@ remappings_regenerate = false slice = "0.0.5" forge-std = "1.9.7" "@openzeppelin-contracts" = "4.8.0" +"@openzeppelin-contracts-upgradeable" = "4.8.0" erc721a = "4.3.0" murky = "0.1.0" diff --git a/soldeer.lock b/soldeer.lock index 2c44d27..9e86754 100644 --- a/soldeer.lock +++ b/soldeer.lock @@ -5,6 +5,13 @@ url = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts/4_8_0_ checksum = "932598a6426b76e315bb9fac536011eb21a76984015efe9e8167c4fc9d7e32a3" integrity = "954367e8adec93f80c6e795012955706347cdb0359360e7c835e4dd29e5a9c2f" +[[dependencies]] +name = "@openzeppelin-contracts-upgradeable" +version = "4.8.0" +url = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts-upgradeable/4_8_0_22-01-2024_13:14:55_contracts-upgradeable.zip" +checksum = "9bd3feb8a6ac529ecf2ab1927bf482bfee9abc2568533b155e567715b83ba94e" +integrity = "16aa3677eec13cfddee8ee412031773a45525d4780b741eab29c746b545afc77" + [[dependencies]] name = "erc721a" version = "4.3.0" diff --git a/test/actions/ERC20Gated/ERC20Gated.t.sol b/test/actions/ERC20Gated/ERC20Gated.t.sol index 3c0b7b6..4bd79f0 100644 --- a/test/actions/ERC20Gated/ERC20Gated.t.sol +++ b/test/actions/ERC20Gated/ERC20Gated.t.sol @@ -11,8 +11,8 @@ uint256 constant productId = 1; contract ERC20GatedTest is RegistryOnchainActionTest { MockERC20Gated erc20Gated; - MockERC20 token = new MockERC20(); - MockERC20 token2 = new MockERC20(); + MockERC20 token = new MockERC20("Test", "TST", 18); + MockERC20 token2 = new MockERC20("Test2", "TST2", 18); function setUp() public { erc20Gated = new MockERC20Gated(PRODUCTS_MODULE); @@ -45,7 +45,7 @@ contract ERC20GatedTest is RegistryOnchainActionTest { assertEq(erc20Gated.gates(slicerId, productId)[1].amount, 200); assertEq(erc20Gated.gates(slicerId, productId).length, 2); - MockERC20 token3 = new MockERC20(); + MockERC20 token3 = new MockERC20("Test3", "TST3", 18); gates = new ERC20Gate[](1); gates[0] = ERC20Gate(token3, 300); diff --git a/test/utils/mocks/MockERC20.sol b/test/utils/mocks/MockERC20.sol index 6f53c15..a3df2b7 100644 --- a/test/utils/mocks/MockERC20.sol +++ b/test/utils/mocks/MockERC20.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {ERC20, IERC20} from "@openzeppelin-4.8.0/token/ERC20/ERC20.sol"; +import "@openzeppelin-4.8.0/token/ERC20/ERC20.sol"; contract MockERC20 is ERC20 { - constructor() ERC20("name", "symbol") {} + constructor(string memory name, string memory symbol, uint8 decimals) ERC20(name, symbol) { + _mint(msg.sender, 1000000 * 10 ** decimals); + } function mint(address to, uint256 amount) external { _mint(to, amount); From 729fdf4442edad0232a6d60a3ce5be1d8bc4b1df Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 6 Aug 2025 16:53:52 +0200 Subject: [PATCH 12/24] update tests --- test/pricingStrategies/VRGDA/LogisticVRGDA.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pricingStrategies/VRGDA/LogisticVRGDA.t.sol b/test/pricingStrategies/VRGDA/LogisticVRGDA.t.sol index 8846949..ff6ecc6 100644 --- a/test/pricingStrategies/VRGDA/LogisticVRGDA.t.sol +++ b/test/pricingStrategies/VRGDA/LogisticVRGDA.t.sol @@ -312,7 +312,7 @@ contract LogisticVRGDATest is RegistryPricingStrategyTest { ); } - function test_RevertOverflow_BeyondLimitTokens(uint256 timeSinceStart, uint256 sold) public { + function testRevert_OverflowBeyondLimitTokens(uint256 timeSinceStart, uint256 sold) public { int256 logisticLimit = toWadUnsafe(MAX_SELLABLE + 1); int256 decayConstant = wadLn(1e18 - priceDecayPercent); From 7d0e6c0a31afd8bf2e2420e55c04a18e4a479bd1 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Mon, 11 Aug 2025 18:33:16 +0200 Subject: [PATCH 13/24] fix: contracts seed and dev env --- script/Seed.s.sol | 71 +++++++++++++++++++++ src/hooks/actions/actions.sol | 8 +++ src/hooks/pricing/pricing.sol | 6 ++ src/hooks/pricingActions/pricingActions.sol | 4 ++ 4 files changed, 89 insertions(+) create mode 100644 script/Seed.s.sol create mode 100644 src/hooks/actions/actions.sol create mode 100644 src/hooks/pricing/pricing.sol create mode 100644 src/hooks/pricingActions/pricingActions.sol diff --git a/script/Seed.s.sol b/script/Seed.s.sol new file mode 100644 index 0000000..5865aac --- /dev/null +++ b/script/Seed.s.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Script} from "forge-std/Script.sol"; +import {CommonStorage} from "slice/utils/CommonStorage.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 { + struct Hook { + address hookAddress; + bytes code; + } + + function _setCode(address target, bytes memory bytecode) internal { + string memory params = string.concat('["', vm.toString(target), '","', vm.toString(bytecode), '"]'); + vm.rpc("anvil_setCode", params); + } + + function run() external { + vm.startBroadcast(); + + Hook[] memory hooks = new Hook[](9); + hooks[0] = Hook({ + hookAddress: 0x157428DD791E03c20880D22C3dA2B66A36B5cF26, + code: address(new Allowlisted(PRODUCTS_MODULE())).code + }); + hooks[1] = Hook({ + hookAddress: 0x26A1C86B555013995Fc72864D261fDe984752E7c, + code: address(new ERC20Gated(PRODUCTS_MODULE())).code + }); + hooks[2] = Hook({ + hookAddress: 0x67f9799FaC1D53C63217BEE47f553150F5BB0836, + code: address(new ERC20Mint(PRODUCTS_MODULE())).code + }); + hooks[3] = Hook({ + hookAddress: 0x2b6488115FAa50142E140172CbCd60e6370675F7, + code: address(new ERC721Mint(PRODUCTS_MODULE())).code + }); + hooks[4] = Hook({ + hookAddress: 0xD4eF7A46bF4c58036eaCA886119F5230e5a2C25d, + code: address(new NFTGated(PRODUCTS_MODULE())).code + }); + hooks[5] = Hook({ + hookAddress: 0xb830a457d2f51d4cA1136b97FB30DF6366CFe2f5, + code: address(new NFTDiscount(PRODUCTS_MODULE())).code + }); + hooks[6] = Hook({ + hookAddress: 0xEC68E30182F4298b7032400B7ce809da613e4449, + code: address(new LinearVRGDAPrices(PRODUCTS_MODULE())).code + }); + hooks[7] = Hook({ + hookAddress: 0x2b02cC8528EF18abf8185543CEC29A94F0542c8F, + code: address(new LogisticVRGDAPrices(PRODUCTS_MODULE())).code + }); + hooks[8] = Hook({ + hookAddress: 0xEC68E30182F4298b7032400B7ce809da613e4449, + code: address(new FirstForFree(PRODUCTS_MODULE())).code + }); + + // Deploy hooks + for (uint256 i = 0; i < hooks.length; i++) { + _setCode(hooks[i].hookAddress, hooks[i].code); + } + + vm.stopBroadcast(); + } +} diff --git a/src/hooks/actions/actions.sol b/src/hooks/actions/actions.sol new file mode 100644 index 0000000..def3994 --- /dev/null +++ b/src/hooks/actions/actions.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Allowlisted} from "./Allowlisted/Allowlisted.sol"; +import {ERC20Gated} from "./ERC20Gated/ERC20Gated.sol"; +import {ERC20Mint} from "./ERC20Mint/ERC20Mint.sol"; +import {ERC721Mint} from "./ERC721Mint/ERC721Mint.sol"; +import {NFTGated} from "./NFTGated/NFTGated.sol"; diff --git a/src/hooks/pricing/pricing.sol b/src/hooks/pricing/pricing.sol new file mode 100644 index 0000000..7a35aa5 --- /dev/null +++ b/src/hooks/pricing/pricing.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {NFTDiscount} from "./TieredDiscount/NFTDiscount/NFTDiscount.sol"; +import {LinearVRGDAPrices} from "./VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol"; +import {LogisticVRGDAPrices} from "./VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol"; diff --git a/src/hooks/pricingActions/pricingActions.sol b/src/hooks/pricingActions/pricingActions.sol new file mode 100644 index 0000000..623127c --- /dev/null +++ b/src/hooks/pricingActions/pricingActions.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {FirstForFree} from "./FirstForFree/FirstForFree.sol"; From 8a567894344557ca1e84c18908510b0970a25a76 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Tue, 19 Aug 2025 02:56:04 +0200 Subject: [PATCH 14/24] WIP on jacopo/contracts-v1.5 --- foundry.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/foundry.toml b/foundry.toml index 8a00afc..e8b4576 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,7 +7,7 @@ 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"}] remappings = [ - "slice/=dependencies/slice-0.0.5/", + "slice/=dependencies/slice-0.0.6/", "@openzeppelin-4.8.0/=dependencies/@openzeppelin-contracts-4.8.0/", "@openzeppelin-upgradeable-4.8.0/=dependencies/@openzeppelin-contracts-upgradeable-4.8.0/", "@erc721a/=dependencies/erc721a-4.3.0/contracts/", @@ -45,7 +45,7 @@ remappings_generate = false remappings_regenerate = false [dependencies] -slice = "0.0.5" +slice = "0.0.6" forge-std = "1.9.7" "@openzeppelin-contracts" = "4.8.0" "@openzeppelin-contracts-upgradeable" = "4.8.0" From 5cf320d2005c0d216c91df6bb13513cc3f38a1f5 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Tue, 19 Aug 2025 16:54:15 +0200 Subject: [PATCH 15/24] various fixes --- foundry.toml | 3 +++ src/hooks/actions/Allowlisted/Allowlisted.sol | 10 ++++++++- .../ERC20Mint/utils/ERC20Mint_BaseToken.sol | 8 +++---- .../ERC721Mint/utils/ERC721Mint_BaseToken.sol | 8 +++---- .../FirstForFree/FirstForFree.sol | 3 +-- test/actions/Allowlisted/Allowlisted.t.sol | 21 ++++++++++++------- 6 files changed, 35 insertions(+), 18 deletions(-) diff --git a/foundry.toml b/foundry.toml index e8b4576..3e54e61 100644 --- a/foundry.toml +++ b/foundry.toml @@ -52,3 +52,6 @@ forge-std = "1.9.7" erc721a = "4.3.0" murky = "0.1.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", "src/examples/actions/BaseGirlsScout.sol"] +exclude_lints=["mixed-case-function", "mixed-case-variable", "pascal-case-struct"] \ No newline at end of file diff --git a/src/hooks/actions/Allowlisted/Allowlisted.sol b/src/hooks/actions/Allowlisted/Allowlisted.sol index cadb88d..273ba16 100644 --- a/src/hooks/actions/Allowlisted/Allowlisted.sol +++ b/src/hooks/actions/Allowlisted/Allowlisted.sol @@ -47,8 +47,16 @@ contract Allowlisted is RegistryOnchainAction { // Get Merkle proof from buyerCustomData bytes32[] memory proof = abi.decode(buyerCustomData, (bytes32[])); + uint256 leafValue = uint256(uint160(account)); + // Generate leaf from account address - bytes32 leaf = keccak256(abi.encodePacked(account)); + bytes32 leaf; + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, leafValue) + leaf := keccak256(0x00, 0x20) + } + bytes32 root = merkleRoots[slicerId][productId]; // Check if Merkle proof is valid diff --git a/src/hooks/actions/ERC20Mint/utils/ERC20Mint_BaseToken.sol b/src/hooks/actions/ERC20Mint/utils/ERC20Mint_BaseToken.sol index 15d873c..a1cc676 100644 --- a/src/hooks/actions/ERC20Mint/utils/ERC20Mint_BaseToken.sol +++ b/src/hooks/actions/ERC20Mint/utils/ERC20Mint_BaseToken.sol @@ -19,7 +19,7 @@ contract ERC20Mint_BaseToken is ERC20 { STORAGE //////////////////////////////////////////////////////////////*/ - address public immutable minter; + address public immutable MINTER; uint256 public maxSupply; /*////////////////////////////////////////////////////////////// @@ -27,7 +27,7 @@ contract ERC20Mint_BaseToken is ERC20 { //////////////////////////////////////////////////////////////*/ constructor(string memory name_, string memory symbol_, uint256 maxSupply_) ERC20(name_, symbol_) { - minter = msg.sender; + MINTER = msg.sender; _setMaxSupply(maxSupply_); } @@ -36,14 +36,14 @@ contract ERC20Mint_BaseToken is ERC20 { //////////////////////////////////////////////////////////////*/ function mint(address to, uint256 amount) public { - if (msg.sender != minter) revert NotMinter(); + if (msg.sender != MINTER) revert NotMinter(); _mint(to, amount); if (totalSupply() > maxSupply) revert MaxSupplyExceeded(); } function setMaxSupply(uint256 maxSupply_) public { - if (msg.sender != minter) revert NotMinter(); + if (msg.sender != MINTER) revert NotMinter(); _setMaxSupply(maxSupply_); } diff --git a/src/hooks/actions/ERC721Mint/utils/ERC721Mint_BaseToken.sol b/src/hooks/actions/ERC721Mint/utils/ERC721Mint_BaseToken.sol index b43f474..3107127 100644 --- a/src/hooks/actions/ERC721Mint/utils/ERC721Mint_BaseToken.sol +++ b/src/hooks/actions/ERC721Mint/utils/ERC721Mint_BaseToken.sol @@ -22,7 +22,7 @@ contract ERC721Mint_BaseToken is ERC721A, IERC2981 { STORAGE //////////////////////////////////////////////////////////////*/ - address public immutable minter; + address public immutable MINTER; uint256 public maxSupply; address public royaltyReceiver; @@ -43,7 +43,7 @@ contract ERC721Mint_BaseToken is ERC721A, IERC2981 { string memory baseURI__, string memory tokenURI__ ) ERC721A(name_, symbol_) { - minter = msg.sender; + MINTER = msg.sender; _setMaxSupply(maxSupply_); royaltyReceiver = royaltyReceiver_; @@ -57,7 +57,7 @@ contract ERC721Mint_BaseToken is ERC721A, IERC2981 { //////////////////////////////////////////////////////////////*/ function mint(address to, uint256 amount) public { - if (msg.sender != minter) revert NotMinter(); + if (msg.sender != MINTER) revert NotMinter(); _mint(to, amount); if (totalSupply() > maxSupply) revert MaxSupplyExceeded(); @@ -70,7 +70,7 @@ contract ERC721Mint_BaseToken is ERC721A, IERC2981 { string memory baseURI__, string memory tokenURI__ ) external { - if (msg.sender != minter) revert NotMinter(); + if (msg.sender != MINTER) revert NotMinter(); royaltyReceiver = royaltyReceiver_; royaltyFraction = royaltyFraction_; diff --git a/src/hooks/pricingActions/FirstForFree/FirstForFree.sol b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol index 8de82ed..0300ff8 100644 --- a/src/hooks/pricingActions/FirstForFree/FirstForFree.sol +++ b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol @@ -3,15 +3,14 @@ pragma solidity ^0.8.20; import {IERC721} from "@openzeppelin-4.8.0/interfaces/IERC721.sol"; import {IERC1155} from "@openzeppelin-4.8.0/interfaces/IERC1155.sol"; -import {IOnchainAction} from "@/interfaces/IOnchainAction.sol"; import { IPricingStrategy, RegistryOnchainAction, RegistryPricingStrategyAction, - HookRegistry, IHookRegistry, IProductsModule } from "@/utils/RegistryPricingStrategyAction.sol"; +import {HookRegistry} from "@/utils/RegistryOnchainAction.sol"; import {ProductParams, TokenCondition} from "./types/ProductParams.sol"; import {TokenType} from "./types/TokenCondition.sol"; import {ITokenERC1155} from "./utils/ITokenERC1155.sol"; diff --git a/test/actions/Allowlisted/Allowlisted.t.sol b/test/actions/Allowlisted/Allowlisted.t.sol index 2678432..187618c 100644 --- a/test/actions/Allowlisted/Allowlisted.t.sol +++ b/test/actions/Allowlisted/Allowlisted.t.sol @@ -19,10 +19,10 @@ contract AllowlistedTest is RegistryOnchainActionTest { m = new Merkle(); data = new bytes32[](4); - data[0] = bytes32(keccak256(abi.encodePacked(buyer))); - data[1] = bytes32(keccak256(abi.encodePacked(address(1)))); - data[2] = bytes32(keccak256(abi.encodePacked(address(2)))); - data[3] = bytes32(keccak256(abi.encodePacked(address(3)))); + data[0] = bytes32(keccak256(abi.encode(buyer))); + data[1] = bytes32(keccak256(abi.encode(address(1)))); + data[2] = bytes32(keccak256(abi.encode(address(2)))); + data[3] = bytes32(keccak256(abi.encode(address(3)))); } function testConfigureProduct() public { @@ -40,10 +40,17 @@ contract AllowlistedTest is RegistryOnchainActionTest { vm.prank(productOwner); allowlisted.configureProduct(slicerId, productId, abi.encode(root)); - bytes32[] memory wrongProof = m.getProof(data, 1); - assertFalse(allowlisted.isPurchaseAllowed(slicerId, productId, buyer, 0, "", abi.encode(wrongProof))); - bytes32[] memory proof = m.getProof(data, 0); assertTrue(allowlisted.isPurchaseAllowed(slicerId, productId, buyer, 0, "", abi.encode(proof))); } + + function testIsPurchaseAllowed_wrongProof() public { + bytes32 root = m.getRoot(data); + + vm.prank(productOwner); + allowlisted.configureProduct(slicerId, productId, abi.encode(root)); + + bytes32[] memory wrongProof = m.getProof(data, 1); + assertFalse(allowlisted.isPurchaseAllowed(slicerId, productId, buyer, 0, "", abi.encode(wrongProof))); + } } From 9184fd88f1e4d92182f5d8d1d3baf1442c00756c Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Tue, 19 Aug 2025 21:47:17 +0200 Subject: [PATCH 16/24] fix --- foundry.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/foundry.toml b/foundry.toml index 3e54e61..626889f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -45,13 +45,13 @@ remappings_generate = false remappings_regenerate = false [dependencies] +murky = "0.1.0" slice = "0.0.6" forge-std = "1.9.7" "@openzeppelin-contracts" = "4.8.0" "@openzeppelin-contracts-upgradeable" = "4.8.0" erc721a = "4.3.0" -murky = "0.1.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", "src/examples/actions/BaseGirlsScout.sol"] -exclude_lints=["mixed-case-function", "mixed-case-variable", "pascal-case-struct"] \ No newline at end of file +exclude_lints=["mixed-case-function", "mixed-case-variable", "pascal-case-struct"] From 3c263b4ab04e75ef902e1820129508c55d198738 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 24 Aug 2025 00:32:05 +0200 Subject: [PATCH 17/24] rename hooks --- .gas-snapshot | 8 ++--- README.md | 32 +++++++++---------- src/examples/README.md | 32 +++++++++---------- src/examples/actions/BaseCafe_2.sol | 8 ++--- src/examples/actions/BaseGirlsScout.sol | 8 ++--- src/hooks/actions/Allowlisted/Allowlisted.sol | 12 +++---- src/hooks/actions/ERC20Gated/ERC20Gated.sol | 12 +++---- src/hooks/actions/ERC20Mint/ERC20Mint.sol | 14 ++++---- src/hooks/actions/ERC721Mint/ERC721Mint.sol | 8 ++--- src/hooks/actions/NFTGated/NFTGated.sol | 12 +++---- src/hooks/actions/README.md | 20 ++++++------ src/hooks/pricing/README.md | 20 ++++++------ .../NFTDiscount/NFTDiscount.sol | 2 +- .../pricing/TieredDiscount/TieredDiscount.sol | 8 ++--- .../LinearVRGDAPrices/LinearVRGDAPrices.sol | 4 +-- .../LogisticVRGDAPrices.sol | 4 +-- src/hooks/pricing/VRGDA/VRGDAPrices.sol | 6 ++-- .../FirstForFree/FirstForFree.sol | 18 +++++------ src/hooks/pricingActions/README.md | 24 +++++++------- ...{IOnchainAction.sol => IProductAction.sol} | 2 +- ...IPricingStrategy.sol => IProductPrice.sol} | 2 +- src/utils/PricingStrategyAction.sol | 4 --- .../{OnchainAction.sol => ProductAction.sol} | 2 +- .../{PricingStrategy.sol => ProductPrice.sol} | 2 +- src/utils/ProductPriceAction.sol | 4 +++ src/utils/RegistryOnchainAction.sol | 4 --- src/utils/RegistryPricingStrategy.sol | 4 --- src/utils/RegistryPricingStrategyAction.sol | 4 --- src/utils/RegistryProductAction.sol | 4 +++ src/utils/RegistryProductPrice.sol | 4 +++ src/utils/RegistryProductPriceAction.sol | 4 +++ test/actions/Allowlisted/Allowlisted.t.sol | 4 +-- test/actions/ERC20Gated/ERC20Gated.t.sol | 4 +-- test/actions/ERC20Mint/ERC20Mint.t.sol | 6 ++-- test/actions/ERC721Mint/ERC721Mint.t.sol | 4 +-- test/actions/NFTGated/NFTGated.t.sol | 4 +-- .../FirstForFree/FirstForFree.t.sol | 4 +-- .../TieredDiscount/NFTDiscount.t.sol | 4 +-- .../pricingStrategies/VRGDA/LinearVRGDA.t.sol | 6 ++-- .../VRGDA/LogisticVRGDA.t.sol | 4 +-- .../correctness/LinearVRGDACorrectness.t.sol | 6 ++-- test/utils/HookRegistryTest.sol | 4 +-- test/utils/HookTest.sol | 2 +- test/utils/OnchainActionTest.sol | 24 -------------- test/utils/PricingStrategyActionTest.sol | 27 ---------------- test/utils/PricingStrategyTest.sol | 11 ------- test/utils/ProductActionTest.sol | 24 ++++++++++++++ test/utils/ProductPriceActionTest.sol | 25 +++++++++++++++ test/utils/ProductPriceTest.sol | 11 +++++++ test/utils/RegistryOnchainActionTest.sol | 17 ---------- .../RegistryPricingStrategyActionTest.sol | 23 ------------- test/utils/RegistryPricingStrategyTest.sol | 12 ------- test/utils/RegistryProductActionTest.sol | 17 ++++++++++ test/utils/RegistryProductPriceActionTest.sol | 20 ++++++++++++ test/utils/RegistryProductPriceTest.sol | 12 +++++++ test/utils/mocks/MockProductsModule.sol | 2 +- 56 files changed, 282 insertions(+), 287 deletions(-) rename src/interfaces/{IOnchainAction.sol => IProductAction.sol} (52%) rename src/interfaces/{IPricingStrategy.sol => IProductPrice.sol} (51%) delete mode 100644 src/utils/PricingStrategyAction.sol rename src/utils/{OnchainAction.sol => ProductAction.sol} (55%) rename src/utils/{PricingStrategy.sol => ProductPrice.sol} (54%) create mode 100644 src/utils/ProductPriceAction.sol delete mode 100644 src/utils/RegistryOnchainAction.sol delete mode 100644 src/utils/RegistryPricingStrategy.sol delete mode 100644 src/utils/RegistryPricingStrategyAction.sol create mode 100644 src/utils/RegistryProductAction.sol create mode 100644 src/utils/RegistryProductPrice.sol create mode 100644 src/utils/RegistryProductPriceAction.sol delete mode 100644 test/utils/OnchainActionTest.sol delete mode 100644 test/utils/PricingStrategyActionTest.sol delete mode 100644 test/utils/PricingStrategyTest.sol create mode 100644 test/utils/ProductActionTest.sol create mode 100644 test/utils/ProductPriceActionTest.sol create mode 100644 test/utils/ProductPriceTest.sol delete mode 100644 test/utils/RegistryOnchainActionTest.sol delete mode 100644 test/utils/RegistryPricingStrategyActionTest.sol delete mode 100644 test/utils/RegistryPricingStrategyTest.sol create mode 100644 test/utils/RegistryProductActionTest.sol create mode 100644 test/utils/RegistryProductPriceActionTest.sol create mode 100644 test/utils/RegistryProductPriceTest.sol diff --git a/.gas-snapshot b/.gas-snapshot index d7386f6..a5ffe9b 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,6 +1,6 @@ LinearVRGDACorrectnessTest:testParamsSchema() (gas: 9581) LinearVRGDACorrectnessTest:testSetup_HookInitialized() (gas: 5671) -LinearVRGDACorrectnessTest:testSupportsInterface_RegistryPricingStrategy() (gas: 9685) +LinearVRGDACorrectnessTest:testSupportsInterface_RegistryProductPrice() (gas: 9685) LinearVRGDATest:testAlwaystargetPriceInRightConditions(uint256) (runs: 256, μ: 14604, ~: 14369) LinearVRGDATest:testParamsSchema() (gas: 9564) LinearVRGDATest:testPricingAdjustedByQuantity() (gas: 18788) @@ -11,7 +11,7 @@ LinearVRGDATest:testProductPriceEth() (gas: 26435) LinearVRGDATest:testProductPriceMultiple() (gas: 29695) LinearVRGDATest:testSetMultiplePrices() (gas: 171215) LinearVRGDATest:testSetup_HookInitialized() (gas: 5715) -LinearVRGDATest:testSupportsInterface_RegistryPricingStrategy() (gas: 9684) +LinearVRGDATest:testSupportsInterface_RegistryProductPrice() (gas: 9684) LinearVRGDATest:testTargetPrice() (gas: 11418) LogisticVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 16809, ~: 16994) LogisticVRGDATest:testGetTargetSaleTimeDoesNotRevertEarly() (gas: 6630) @@ -27,7 +27,7 @@ LogisticVRGDATest:testProductPriceEth() (gas: 28425) LogisticVRGDATest:testProductPriceMultiple() (gas: 34309) LogisticVRGDATest:testSetMultiplePrices() (gas: 181254) LogisticVRGDATest:testSetup_HookInitialized() (gas: 5693) -LogisticVRGDATest:testSupportsInterface_RegistryPricingStrategy() (gas: 9752) +LogisticVRGDATest:testSupportsInterface_RegistryProductPrice() (gas: 9752) LogisticVRGDATest:testTargetPrice() (gas: 13363) LogisticVRGDATest:test_RevertOverflow_BeyondLimitTokens(uint256,uint256) (runs: 256, μ: 14963, ~: 14978) NFTDiscountTest:testDeploy() (gas: 5246) @@ -44,4 +44,4 @@ NFTDiscountTest:testSetProductPrice__Edit_Add() (gas: 249809) NFTDiscountTest:testSetProductPrice__Edit_Remove() (gas: 230519) NFTDiscountTest:testSetProductPrice__MultipleCurrencies() (gas: 198172) NFTDiscountTest:testSetup_HookInitialized() (gas: 5715) -NFTDiscountTest:testSupportsInterface_RegistryPricingStrategy() (gas: 9705) \ No newline at end of file +NFTDiscountTest:testSupportsInterface_RegistryProductPrice() (gas: 9705) \ No newline at end of file diff --git a/README.md b/README.md index 96c663b..e1da422 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ src/ Hooks are built around three main interfaces: -- **[`IOnchainAction`](./src/interfaces/IOnchainAction.sol)**: Execute custom logic during purchases (eligibility checks, rewards, etc.) -- **[`IPricingStrategy`](./src/interfaces/IPricingStrategy.sol)**: Calculate dynamic prices for products +- **[`IProductAction`](./src/interfaces/IProductAction.sol)**: Execute custom logic during purchases (eligibility checks, rewards, etc.) +- **[`IProductPrice`](./src/interfaces/IProductPrice.sol)**: Calculate dynamic prices for products - **[`IHookRegistry`](./src/interfaces/IHookRegistry.sol)**: Enable reusable hooks across multiple products with frontend integration Hooks can be: @@ -42,13 +42,13 @@ Here's how hooks integrate into the product purchase flow: ▼ ┌─────────────────────┐ │ Price Fetching │ ← `productPrice` called here -│ (before purchase) │ (IPricingStrategy) +│ (before purchase) │ (IProductPrice) └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ Purchase Execution │ ← `onProductPurchase` called here -│ (during purchase) │ (IOnchainAction) +│ (during purchase) │ (IProductAction) └─────────────────────┘ │ ▼ @@ -85,15 +85,15 @@ The base contracts in `src/utils` are designed to be inherited, providing essent ### Registry (Reusable): -- **`RegistryOnchainAction`**: Base for reusable onchain actions -- **`RegistryPricingStrategy`**: Base for reusable pricing strategies -- **`RegistryPricingStrategyAction`**: Base for reusable pricing + action hooks +- **`RegistryProductAction`**: Base for reusable onchain actions +- **`RegistryProductPrice`**: Base for reusable pricing strategies +- **`RegistryProductPriceAction`**: Base for reusable pricing + action hooks ### Product-Specific -- **`OnchainAction`**: Base for product-specific onchain actions -- **`PricingStrategy`**: Base for product-specific pricing strategies -- **`PricingStrategyAction`**: Base for product-specific pricing + action hooks +- **`ProductAction`**: Base for product-specific onchain actions +- **`ProductPrice`**: Base for product-specific pricing strategies +- **`ProductPriceAction`**: Base for product-specific pricing + action hooks ## Quick Start @@ -126,12 +126,12 @@ The script will present you with a list of available contracts to deploy. Select When writing tests for your hooks, inherit from the appropriate base test contract: -- **`RegistryOnchainActionTest`**: For testing `RegistryOnchainAction` contracts -- **`RegistryPricingStrategyTest`**: For testing `RegistryPricingStrategy` contracts -- **`RegistryPricingStrategyActionTest`**: For testing `RegistryPricingStrategyAction` contracts -- **`OnchainActionTest`**: For testing `OnchainAction` contracts -- **`PricingStrategyTest`**: For testing `PricingStrategy` contracts -- **`PricingStrategyActionTest`**: For testing `PricingStrategyAction` contracts +- **`RegistryProductActionTest`**: For testing `RegistryProductAction` contracts +- **`RegistryProductPriceTest`**: For testing `RegistryProductPrice` contracts +- **`RegistryProductPriceActionTest`**: For testing `RegistryProductPriceAction` contracts +- **`ProductActionTest`**: For testing `ProductAction` contracts +- **`ProductPriceTest`**: For testing `ProductPrice` contracts +- **`ProductPriceActionTest`**: For testing `ProductPriceAction` contracts Inheriting the appropriate test contract for your hook allows you to focus your tests solely on your custom hook logic. diff --git a/src/examples/README.md b/src/examples/README.md index 6861892..c42f2ef 100644 --- a/src/examples/README.md +++ b/src/examples/README.md @@ -4,9 +4,9 @@ This folder contains product-specific smart contract implementations that demons ## Key Interfaces -**IPricingStrategy**: +**IProductPrice**: ```solidity -interface IPricingStrategy { +interface IProductPrice { function productPrice( uint256 slicerId, uint256 productId, @@ -18,9 +18,9 @@ interface IPricingStrategy { } ``` -**IOnchainAction**: +**IProductAction**: ```solidity -interface IOnchainAction { +interface IProductAction { function isPurchaseAllowed( uint256 slicerId, uint256 productId, @@ -43,15 +43,15 @@ interface IOnchainAction { ## Base Contracts -- **OnchainAction**: Add arbitrary requirements and/or custom logic after product purchase. -- **PricingStrategy**: Customize product pricing logic. -- **PricingStrategyAction**: Provide functionality of both Onchain Actions and Pricing Strategies +- **ProductAction**: Add arbitrary requirements and/or custom logic after product purchase. +- **ProductPrice**: Customize product pricing logic. +- **ProductPriceAction**: Provide functionality of both Onchain Actions and Pricing Strategies ## Key Differences from Registry Hooks Unlike the reusable hooks in `/hooks/`, these examples: - Are tailored for specific products/projects -- Inherit directly from base contracts (`OnchainAction`, `PricingStrategy`) +- Inherit directly from base contracts (`ProductAction`, `ProductPrice`) - Don't implement `IHookRegistry` (not intended for Slice frontend integration) - Serve as reference implementations and starting points @@ -68,13 +68,13 @@ Unlike the reusable hooks in `/hooks/`, these examples: To create a custom product-specific onchain action: -1. **Inherit from OnchainAction**: +1. **Inherit from ProductAction**: ```solidity -import {OnchainAction, IProductsModule} from "@/utils/OnchainAction.sol"; +import {ProductAction, IProductsModule} from "@/utils/ProductAction.sol"; -contract MyProductAction is OnchainAction { +contract MyProductAction is ProductAction { constructor(IProductsModule productsModule, uint256 slicerId) - OnchainAction(productsModule, slicerId) {} + ProductAction(productsModule, slicerId) {} } ``` @@ -108,13 +108,13 @@ function isPurchaseAllowed( To create a custom product-specific pricing strategy: -1. **Inherit from PricingStrategy**: +1. **Inherit from ProductPrice**: ```solidity -import {PricingStrategy, IProductsModule} from "@/utils/PricingStrategy.sol"; +import {ProductPrice, IProductsModule} from "@/utils/ProductPrice.sol"; -contract MyProductAction is PricingStrategy { +contract MyProductAction is ProductPrice { constructor(IProductsModule productsModule, uint256 slicerId) - PricingStrategy(productsModule, slicerId) {} + ProductPrice(productsModule, slicerId) {} } ``` diff --git a/src/examples/actions/BaseCafe_2.sol b/src/examples/actions/BaseCafe_2.sol index 6dcc920..d7da230 100644 --- a/src/examples/actions/BaseCafe_2.sol +++ b/src/examples/actions/BaseCafe_2.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {IProductsModule, OnchainAction} from "@/utils/OnchainAction.sol"; +import {IProductsModule, ProductAction} from "@/utils/ProductAction.sol"; /** * @title BaseCafe * @notice Onchain action that mints an NFT to the buyer on every purchase. * @author Slice */ -contract BaseCafe is OnchainAction { +contract BaseCafe is ProductAction { /*////////////////////////////////////////////////////////////// IMMUTABLE STORAGE //////////////////////////////////////////////////////////////*/ @@ -21,7 +21,7 @@ contract BaseCafe is OnchainAction { //////////////////////////////////////////////////////////////*/ constructor(IProductsModule productsModuleAddress, uint256 slicerId) - OnchainAction(productsModuleAddress, slicerId) + ProductAction(productsModuleAddress, slicerId) {} /*////////////////////////////////////////////////////////////// @@ -29,7 +29,7 @@ contract BaseCafe is OnchainAction { //////////////////////////////////////////////////////////////*/ /** - * @inheritdoc OnchainAction + * @inheritdoc ProductAction * @notice Mint `quantity` NFTs to `account` on purchase */ function _onProductPurchase(uint256, uint256, address buyer, uint256 quantity, bytes memory, bytes memory) diff --git a/src/examples/actions/BaseGirlsScout.sol b/src/examples/actions/BaseGirlsScout.sol index a64517e..9ca7860 100644 --- a/src/examples/actions/BaseGirlsScout.sol +++ b/src/examples/actions/BaseGirlsScout.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {IProductsModule, OnchainAction} from "@/utils/OnchainAction.sol"; +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"; @@ -10,7 +10,7 @@ import {IERC1155} from "@openzeppelin-4.8.0/interfaces/IERC1155.sol"; * @notice Onchain action that mints Base Girls Scout NFTs to the buyer on every purchase. * @author Slice */ -contract BaseGirlsScout is OnchainAction, Ownable { +contract BaseGirlsScout is ProductAction, Ownable { /*////////////////////////////////////////////////////////////// IMMUTABLE STORAGE //////////////////////////////////////////////////////////////*/ @@ -29,7 +29,7 @@ contract BaseGirlsScout is OnchainAction, Ownable { //////////////////////////////////////////////////////////////*/ constructor(IProductsModule productsModuleAddress, uint256 slicerId) - OnchainAction(productsModuleAddress, slicerId) + ProductAction(productsModuleAddress, slicerId) Ownable() { allowedSlicerIds[2217] = true; @@ -41,7 +41,7 @@ contract BaseGirlsScout is OnchainAction, Ownable { //////////////////////////////////////////////////////////////*/ /** - * @inheritdoc OnchainAction + * @inheritdoc ProductAction * @notice Mint `quantity` NFTs to `account` on purchase */ function _onProductPurchase(uint256, uint256, address buyer, uint256 quantity, bytes memory, bytes memory) diff --git a/src/hooks/actions/Allowlisted/Allowlisted.sol b/src/hooks/actions/Allowlisted/Allowlisted.sol index 273ba16..5240217 100644 --- a/src/hooks/actions/Allowlisted/Allowlisted.sol +++ b/src/hooks/actions/Allowlisted/Allowlisted.sol @@ -4,18 +4,18 @@ pragma solidity ^0.8.20; import {MerkleProof} from "@openzeppelin-4.8.0/utils/cryptography/MerkleProof.sol"; import { IProductsModule, - RegistryOnchainAction, + RegistryProductAction, HookRegistry, - IOnchainAction, + IProductAction, IHookRegistry -} from "@/utils/RegistryOnchainAction.sol"; +} from "@/utils/RegistryProductAction.sol"; /** * @title Allowlisted * @notice Onchain action registry for allowlist requirement. * @author Slice */ -contract Allowlisted is RegistryOnchainAction { +contract Allowlisted is RegistryProductAction { /*////////////////////////////////////////////////////////////// MUTABLE STORAGE //////////////////////////////////////////////////////////////*/ @@ -26,14 +26,14 @@ contract Allowlisted is RegistryOnchainAction { CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(IProductsModule productsModuleAddress) RegistryOnchainAction(productsModuleAddress) {} + constructor(IProductsModule productsModuleAddress) RegistryProductAction(productsModuleAddress) {} /*////////////////////////////////////////////////////////////// CONFIGURATION //////////////////////////////////////////////////////////////*/ /** - * @inheritdoc IOnchainAction + * @inheritdoc IProductAction * @dev Checks if the account is in the allowlist. */ function isPurchaseAllowed( diff --git a/src/hooks/actions/ERC20Gated/ERC20Gated.sol b/src/hooks/actions/ERC20Gated/ERC20Gated.sol index 13e09a5..6a897ab 100644 --- a/src/hooks/actions/ERC20Gated/ERC20Gated.sol +++ b/src/hooks/actions/ERC20Gated/ERC20Gated.sol @@ -4,18 +4,18 @@ pragma solidity ^0.8.20; import {ERC20Gate} from "./types/ERC20Gate.sol"; import { IProductsModule, - RegistryOnchainAction, + RegistryProductAction, HookRegistry, - IOnchainAction, + IProductAction, IHookRegistry -} from "@/utils/RegistryOnchainAction.sol"; +} from "@/utils/RegistryProductAction.sol"; /** * @title ERC20Gated * @notice Onchain action registry for ERC20 gating. * @author Slice */ -contract ERC20Gated is RegistryOnchainAction { +contract ERC20Gated is RegistryProductAction { /*////////////////////////////////////////////////////////////// MUTABLE STORAGE //////////////////////////////////////////////////////////////*/ @@ -26,14 +26,14 @@ contract ERC20Gated is RegistryOnchainAction { CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(IProductsModule productsModuleAddress) RegistryOnchainAction(productsModuleAddress) {} + constructor(IProductsModule productsModuleAddress) RegistryProductAction(productsModuleAddress) {} /*////////////////////////////////////////////////////////////// CONFIGURATION //////////////////////////////////////////////////////////////*/ /** - * @inheritdoc IOnchainAction + * @inheritdoc IProductAction * @dev Checks if `account` owns the required amount of all ERC20 tokens. */ function isPurchaseAllowed( diff --git a/src/hooks/actions/ERC20Mint/ERC20Mint.sol b/src/hooks/actions/ERC20Mint/ERC20Mint.sol index cde3489..002da81 100644 --- a/src/hooks/actions/ERC20Mint/ERC20Mint.sol +++ b/src/hooks/actions/ERC20Mint/ERC20Mint.sol @@ -3,11 +3,11 @@ pragma solidity ^0.8.20; import { IProductsModule, - RegistryOnchainAction, + RegistryProductAction, HookRegistry, - IOnchainAction, + IProductAction, IHookRegistry -} from "@/utils/RegistryOnchainAction.sol"; +} from "@/utils/RegistryProductAction.sol"; import {ERC20Data} from "./types/ERC20Data.sol"; import {ERC20Mint_BaseToken} from "./utils/ERC20Mint_BaseToken.sol"; @@ -17,7 +17,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 RegistryOnchainAction { +contract ERC20Mint is RegistryProductAction { /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ @@ -34,14 +34,14 @@ contract ERC20Mint is RegistryOnchainAction { CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(IProductsModule productsModuleAddress) RegistryOnchainAction(productsModuleAddress) {} + constructor(IProductsModule productsModuleAddress) RegistryProductAction(productsModuleAddress) {} /*////////////////////////////////////////////////////////////// CONFIGURATION //////////////////////////////////////////////////////////////*/ /** - * @inheritdoc IOnchainAction + * @inheritdoc IProductAction * @dev If `revertOnMaxSupplyReached` is set to true, returns false when max supply is exceeded. */ function isPurchaseAllowed( @@ -63,7 +63,7 @@ contract ERC20Mint is RegistryOnchainAction { } /** - * @inheritdoc RegistryOnchainAction + * @inheritdoc RegistryProductAction * @notice Mint tokens to the buyer. * @dev If `revertOnMaxSupplyReached` is set to true, reverts when max supply is exceeded. */ diff --git a/src/hooks/actions/ERC721Mint/ERC721Mint.sol b/src/hooks/actions/ERC721Mint/ERC721Mint.sol index a606d5b..6e70849 100644 --- a/src/hooks/actions/ERC721Mint/ERC721Mint.sol +++ b/src/hooks/actions/ERC721Mint/ERC721Mint.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {IProductsModule, RegistryOnchainAction, HookRegistry, IHookRegistry} from "@/utils/RegistryOnchainAction.sol"; +import {IProductsModule, RegistryProductAction, HookRegistry, IHookRegistry} from "@/utils/RegistryProductAction.sol"; import {MAX_ROYALTY, ERC721Mint_BaseToken} from "./utils/ERC721Mint_BaseToken.sol"; import {ERC721Data} from "./types/ERC721Data.sol"; @@ -11,7 +11,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 RegistryOnchainAction { +contract ERC721Mint is RegistryProductAction { /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ @@ -29,14 +29,14 @@ contract ERC721Mint is RegistryOnchainAction { CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(IProductsModule productsModuleAddress) RegistryOnchainAction(productsModuleAddress) {} + constructor(IProductsModule productsModuleAddress) RegistryProductAction(productsModuleAddress) {} /*////////////////////////////////////////////////////////////// CONFIGURATION //////////////////////////////////////////////////////////////*/ /** - * @inheritdoc RegistryOnchainAction + * @inheritdoc RegistryProductAction * @notice Mint tokens to the buyer. * @dev If `revertOnMaxSupplyReached` is set to true, reverts when max supply is exceeded. */ diff --git a/src/hooks/actions/NFTGated/NFTGated.sol b/src/hooks/actions/NFTGated/NFTGated.sol index ab3c614..149c610 100644 --- a/src/hooks/actions/NFTGated/NFTGated.sol +++ b/src/hooks/actions/NFTGated/NFTGated.sol @@ -5,11 +5,11 @@ import {IERC721} from "@openzeppelin-4.8.0/interfaces/IERC721.sol"; import {IERC1155} from "@openzeppelin-4.8.0/interfaces/IERC1155.sol"; import { IProductsModule, - RegistryOnchainAction, + RegistryProductAction, HookRegistry, - IOnchainAction, + IProductAction, IHookRegistry -} from "@/utils/RegistryOnchainAction.sol"; +} from "@/utils/RegistryProductAction.sol"; import {NftType, NFTGate, NFTGates} from "./types/NFTGate.sol"; /** @@ -17,7 +17,7 @@ import {NftType, NFTGate, NFTGates} from "./types/NFTGate.sol"; * @notice Onchain action registry for NFT gating. * @author Slice */ -contract NFTGated is RegistryOnchainAction { +contract NFTGated is RegistryProductAction { /*////////////////////////////////////////////////////////////// MUTABLE STORAGE //////////////////////////////////////////////////////////////*/ @@ -28,14 +28,14 @@ contract NFTGated is RegistryOnchainAction { CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(IProductsModule productsModuleAddress) RegistryOnchainAction(productsModuleAddress) {} + constructor(IProductsModule productsModuleAddress) RegistryProductAction(productsModuleAddress) {} /*////////////////////////////////////////////////////////////// CONFIGURATION //////////////////////////////////////////////////////////////*/ /** - * @inheritdoc IOnchainAction + * @inheritdoc IProductAction * @dev Checks if `account` owns the required amount of NFT tokens. */ function isPurchaseAllowed( diff --git a/src/hooks/actions/README.md b/src/hooks/actions/README.md index 9adb197..f109ee7 100644 --- a/src/hooks/actions/README.md +++ b/src/hooks/actions/README.md @@ -1,11 +1,11 @@ # Onchain Actions -Onchain actions are smart contracts that execute custom logic when products are purchased on Slice. They implement the `IOnchainAction` interface and can control purchase eligibility and perform actions after purchases. +Onchain actions are smart contracts that execute custom logic when products are purchased on Slice. They implement the `IProductAction` interface and can control purchase eligibility and perform actions after purchases. -## Key Interface: IOnchainAction +## Key Interface: IProductAction ```solidity -interface IOnchainAction { +interface IProductAction { function isPurchaseAllowed( uint256 slicerId, uint256 productId, @@ -26,9 +26,9 @@ interface IOnchainAction { } ``` -## Base Contract: RegistryOnchainAction +## Base Contract: RegistryProductAction -All actions in this directory inherit from `RegistryOnchainAction`, which provides: +All actions in this directory inherit from `RegistryProductAction`, which provides: - Registry functionality for reusable hooks across multiple products - Implementation of `IHookRegistry` for Slice frontend integration - Base implementations for common patterns @@ -45,13 +45,13 @@ All actions in this directory inherit from `RegistryOnchainAction`, which provid To create a custom onchain action: -1. **Inherit from RegistryOnchainAction**: +1. **Inherit from RegistryProductAction**: ```solidity -import {RegistryOnchainAction, IProductsModule} from "@/utils/RegistryOnchainAction.sol"; +import {RegistryProductAction, IProductsModule} from "@/utils/RegistryProductAction.sol"; -contract MyAction is RegistryOnchainAction { +contract MyAction is RegistryProductAction { constructor(IProductsModule productsModule) - RegistryOnchainAction(productsModule) {} + RegistryProductAction(productsModule) {} } ``` @@ -77,7 +77,7 @@ function paramsSchema() external pure override returns (string memory) { ## Integration with Slice -Actions that inherit from `RegistryOnchainAction` are automatically compatible with Slice frontends through the `IHookRegistry` interface, enabling: +Actions that inherit from `RegistryProductAction` are automatically compatible with Slice frontends through the `IHookRegistry` interface, enabling: - Product configuration via `configureProduct()` - Parameter validation via `paramsSchema()` - Automatic discovery and integration \ No newline at end of file diff --git a/src/hooks/pricing/README.md b/src/hooks/pricing/README.md index 2534bba..7bbf2ef 100644 --- a/src/hooks/pricing/README.md +++ b/src/hooks/pricing/README.md @@ -1,11 +1,11 @@ # Pricing Strategies -Pricing strategies are smart contracts that calculate dynamic prices for products on Slice. They implement the `IPricingStrategy` interface to provide custom pricing logic based on arbitrary factors and conditions. +Pricing strategies are smart contracts that calculate dynamic prices for products on Slice. They implement the `IProductPrice` interface to provide custom pricing logic based on arbitrary factors and conditions. -## Key Interface: IPricingStrategy +## Key Interface: IProductPrice ```solidity -interface IPricingStrategy { +interface IProductPrice { function productPrice( uint256 slicerId, uint256 productId, @@ -17,9 +17,9 @@ interface IPricingStrategy { } ``` -## Base Contract: RegistryPricingStrategy +## Base Contract: RegistryProductPrice -All pricing strategies in this directory inherit from `RegistryPricingStrategy`, which provides: +All pricing strategies in this directory inherit from `RegistryProductPrice`, which provides: - Registry functionality for reusable pricing across multiple products - Implementation of `IHookRegistry` for Slice frontend integration - Base implementations for common patterns @@ -35,13 +35,13 @@ All pricing strategies in this directory inherit from `RegistryPricingStrategy`, To create a custom pricing strategy: -1. **Inherit from RegistryPricingStrategy**: +1. **Inherit from RegistryProductPrice**: ```solidity -import {RegistryPricingStrategy, IProductsModule} from "@/utils/RegistryPricingStrategy.sol"; +import {RegistryProductPrice, IProductsModule} from "@/utils/RegistryProductPrice.sol"; -contract MyPricingStrategy is RegistryPricingStrategy { +contract MyProductPrice is RegistryProductPrice { constructor(IProductsModule productsModule) - RegistryPricingStrategy(productsModule) {} + RegistryProductPrice(productsModule) {} } ``` @@ -63,7 +63,7 @@ function paramsSchema() external pure override returns (string memory) { ## Integration with Slice -Pricing strategies that inherit from `RegistryPricingStrategy` are automatically compatible with Slice frontends through the `IHookRegistry` interface, enabling: +Pricing strategies that inherit from `RegistryProductPrice` are automatically compatible with Slice frontends through the `IHookRegistry` interface, enabling: - Product configuration via `configureProduct()` - Parameter validation via `paramsSchema()` - Automatic discovery and integration \ No newline at end of file diff --git a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol index 2e7edfe..a118958 100644 --- a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; 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/RegistryPricingStrategy.sol"; +import {HookRegistry, IHookRegistry, IProductsModule} from "@/utils/RegistryProductPrice.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 8d2a3d8..ce1d270 100644 --- a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; import {IProductsModule} from "slice/interfaces/IProductsModule.sol"; -import {RegistryPricingStrategy, IPricingStrategy} from "@/utils/RegistryPricingStrategy.sol"; +import {RegistryProductPrice, IProductPrice} from "@/utils/RegistryProductPrice.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 RegistryPricingStrategy { +abstract contract TieredDiscount is RegistryProductPrice { /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ @@ -30,14 +30,14 @@ abstract contract TieredDiscount is RegistryPricingStrategy { CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(IProductsModule productsModuleAddress) RegistryPricingStrategy(productsModuleAddress) {} + constructor(IProductsModule productsModuleAddress) RegistryProductPrice(productsModuleAddress) {} /*////////////////////////////////////////////////////////////// FUNCTIONS //////////////////////////////////////////////////////////////*/ /** - * @inheritdoc IPricingStrategy + * @inheritdoc IProductPrice */ function productPrice( uint256 slicerId, diff --git a/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol index cc56197..cc6b61a 100644 --- a/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {HookRegistry, IPricingStrategy, IHookRegistry, IProductsModule} from "@/utils/RegistryPricingStrategy.sol"; +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"; @@ -64,7 +64,7 @@ contract LinearVRGDAPrices is VRGDAPrices { } /** - * @inheritdoc IPricingStrategy + * @inheritdoc IProductPrice */ function productPrice( uint256 slicerId, diff --git a/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol b/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol index 52f5aa7..6e0bb39 100644 --- a/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {HookRegistry, IPricingStrategy, IHookRegistry, IProductsModule} from "@/utils/RegistryPricingStrategy.sol"; +import {HookRegistry, IProductPrice, IHookRegistry, IProductsModule} from "@/utils/RegistryProductPrice.sol"; import { wadMul, toWadUnsafe, @@ -72,7 +72,7 @@ contract LogisticVRGDAPrices is VRGDAPrices { } /** - * @inheritdoc IPricingStrategy + * @inheritdoc IProductPrice */ function productPrice( uint256 slicerId, diff --git a/src/hooks/pricing/VRGDA/VRGDAPrices.sol b/src/hooks/pricing/VRGDA/VRGDAPrices.sol index 27cb5da..a8ffd55 100644 --- a/src/hooks/pricing/VRGDA/VRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/VRGDAPrices.sol @@ -2,19 +2,19 @@ pragma solidity ^0.8.20; import {wadExp, wadMul, unsafeWadMul, toWadUnsafe} from "@/utils/math/SignedWadMath.sol"; -import {IProductsModule, RegistryPricingStrategy} from "@/utils/RegistryPricingStrategy.sol"; +import {IProductsModule, RegistryProductPrice} from "@/utils/RegistryProductPrice.sol"; /** * @title VRGDAPrices * @notice Variable Rate Gradual Dutch Auction * @author Slice */ -abstract contract VRGDAPrices is RegistryPricingStrategy { +abstract contract VRGDAPrices is RegistryProductPrice { /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(IProductsModule productsModuleAddress) RegistryPricingStrategy(productsModuleAddress) {} + constructor(IProductsModule productsModuleAddress) RegistryProductPrice(productsModuleAddress) {} /*////////////////////////////////////////////////////////////// PRICING LOGIC diff --git a/src/hooks/pricingActions/FirstForFree/FirstForFree.sol b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol index 0300ff8..f9ec506 100644 --- a/src/hooks/pricingActions/FirstForFree/FirstForFree.sol +++ b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol @@ -4,13 +4,13 @@ pragma solidity ^0.8.20; import {IERC721} from "@openzeppelin-4.8.0/interfaces/IERC721.sol"; import {IERC1155} from "@openzeppelin-4.8.0/interfaces/IERC1155.sol"; import { - IPricingStrategy, - RegistryOnchainAction, - RegistryPricingStrategyAction, + IProductPrice, + RegistryProductAction, + RegistryProductPriceAction, IHookRegistry, IProductsModule -} from "@/utils/RegistryPricingStrategyAction.sol"; -import {HookRegistry} from "@/utils/RegistryOnchainAction.sol"; +} from "@/utils/RegistryProductPriceAction.sol"; +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"; @@ -20,7 +20,7 @@ import {ITokenERC1155} from "./utils/ITokenERC1155.sol"; * @notice Discounts the first purchase of a product for free, based on conditions. * @author Slice */ -contract FirstForFree is RegistryPricingStrategyAction { +contract FirstForFree is RegistryProductPriceAction { /*////////////////////////////////////////////////////////////// MUTABLE STORAGE //////////////////////////////////////////////////////////////*/ @@ -32,14 +32,14 @@ contract FirstForFree is RegistryPricingStrategyAction { CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(IProductsModule productsModuleAddress) RegistryPricingStrategyAction(productsModuleAddress) {} + constructor(IProductsModule productsModuleAddress) RegistryProductPriceAction(productsModuleAddress) {} /*////////////////////////////////////////////////////////////// CONFIGURATION //////////////////////////////////////////////////////////////*/ /** - * @inheritdoc IPricingStrategy + * @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) @@ -68,7 +68,7 @@ contract FirstForFree is RegistryPricingStrategyAction { } /** - * @inheritdoc RegistryOnchainAction + * @inheritdoc RegistryProductAction * @notice Mint `quantity` NFTs to `account` on purchase. Keeps track of total purchases. */ function _onProductPurchase( diff --git a/src/hooks/pricingActions/README.md b/src/hooks/pricingActions/README.md index 3a1165d..9221111 100644 --- a/src/hooks/pricingActions/README.md +++ b/src/hooks/pricingActions/README.md @@ -1,12 +1,12 @@ # Pricing Strategy Actions -Pricing strategy actions combine both pricing strategies and onchain actions in a single contract. They implement both `IPricingStrategy` and `IOnchainAction` interfaces, allowing them to calculate dynamic prices AND execute custom logic during purchases. +Pricing strategy actions combine both pricing strategies and onchain actions in a single contract. They implement both `IProductPrice` and `IProductAction` interfaces, allowing them to calculate dynamic prices AND execute custom logic during purchases. ## Key Interfaces -**IPricingStrategy**: +**IProductPrice**: ```solidity -interface IPricingStrategy { +interface IProductPrice { function productPrice( uint256 slicerId, uint256 productId, @@ -18,9 +18,9 @@ interface IPricingStrategy { } ``` -**IOnchainAction**: +**IProductAction**: ```solidity -interface IOnchainAction { +interface IProductAction { function isPurchaseAllowed( uint256 slicerId, uint256 productId, @@ -41,9 +41,9 @@ interface IOnchainAction { } ``` -## Base Contract: RegistryPricingStrategyAction +## Base Contract: RegistryProductPriceAction -All pricing strategy actions inherit from `RegistryPricingStrategyAction`, which provides: +All pricing strategy actions inherit from `RegistryProductPriceAction`, which provides: - Combined functionality of both pricing strategies and onchain actions - Registry functionality for reusable hooks across multiple products - Implementation of `IHookRegistry` for Slice frontend integration @@ -56,13 +56,13 @@ All pricing strategy actions inherit from `RegistryPricingStrategyAction`, which To create a custom pricing strategy action: -1. **Inherit from RegistryPricingStrategyAction**: +1. **Inherit from RegistryProductPriceAction**: ```solidity -import {RegistryPricingStrategyAction, IProductsModule} from "@/utils/RegistryPricingStrategyAction.sol"; +import {RegistryProductPriceAction, IProductsModule} from "@/utils/RegistryProductPriceAction.sol"; -contract MyPricingStrategyAction is RegistryPricingStrategyAction { +contract MyProductPriceAction is RegistryProductPriceAction { constructor(IProductsModule productsModule) - RegistryPricingStrategyAction(productsModule) {} + RegistryProductPriceAction(productsModule) {} } ``` @@ -92,7 +92,7 @@ function paramsSchema() external pure override returns (string memory) { ## Integration with Slice -Pricing strategy actions that inherit from `RegistryPricingStrategyAction` are automatically compatible with Slice frontends through the `IHookRegistry` interface, enabling: +Pricing strategy actions that inherit from `RegistryProductPriceAction` are automatically compatible with Slice frontends through the `IHookRegistry` interface, enabling: - Product configuration via `configureProduct()` - Parameter validation via `paramsSchema()` - Automatic discovery and integration \ No newline at end of file diff --git a/src/interfaces/IOnchainAction.sol b/src/interfaces/IProductAction.sol similarity index 52% rename from src/interfaces/IOnchainAction.sol rename to src/interfaces/IProductAction.sol index 15fcc3b..c950a3c 100644 --- a/src/interfaces/IOnchainAction.sol +++ b/src/interfaces/IProductAction.sol @@ -1,4 +1,4 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import "slice/interfaces/hooks/IOnchainAction.sol"; +import "slice/interfaces/hooks/IProductAction.sol"; diff --git a/src/interfaces/IPricingStrategy.sol b/src/interfaces/IProductPrice.sol similarity index 51% rename from src/interfaces/IPricingStrategy.sol rename to src/interfaces/IProductPrice.sol index 94f2594..898fcca 100644 --- a/src/interfaces/IPricingStrategy.sol +++ b/src/interfaces/IProductPrice.sol @@ -1,4 +1,4 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import "slice/interfaces/hooks/IPricingStrategy.sol"; +import "slice/interfaces/hooks/IProductPrice.sol"; diff --git a/src/utils/PricingStrategyAction.sol b/src/utils/PricingStrategyAction.sol deleted file mode 100644 index cebf111..0000000 --- a/src/utils/PricingStrategyAction.sol +++ /dev/null @@ -1,4 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "slice/utils/hooks/PricingStrategyAction.sol"; diff --git a/src/utils/OnchainAction.sol b/src/utils/ProductAction.sol similarity index 55% rename from src/utils/OnchainAction.sol rename to src/utils/ProductAction.sol index 7843e87..cd76334 100644 --- a/src/utils/OnchainAction.sol +++ b/src/utils/ProductAction.sol @@ -1,4 +1,4 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import "slice/utils/hooks/OnchainAction.sol"; +import "slice/utils/hooks/ProductAction.sol"; diff --git a/src/utils/PricingStrategy.sol b/src/utils/ProductPrice.sol similarity index 54% rename from src/utils/PricingStrategy.sol rename to src/utils/ProductPrice.sol index 66f2d12..0192baa 100644 --- a/src/utils/PricingStrategy.sol +++ b/src/utils/ProductPrice.sol @@ -1,4 +1,4 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import "slice/utils/hooks/PricingStrategy.sol"; +import "slice/utils/hooks/ProductPrice.sol"; diff --git a/src/utils/ProductPriceAction.sol b/src/utils/ProductPriceAction.sol new file mode 100644 index 0000000..4acadb5 --- /dev/null +++ b/src/utils/ProductPriceAction.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "slice/utils/hooks/ProductPriceAction.sol"; diff --git a/src/utils/RegistryOnchainAction.sol b/src/utils/RegistryOnchainAction.sol deleted file mode 100644 index 679480d..0000000 --- a/src/utils/RegistryOnchainAction.sol +++ /dev/null @@ -1,4 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "slice/utils/hooks/RegistryOnchainAction.sol"; diff --git a/src/utils/RegistryPricingStrategy.sol b/src/utils/RegistryPricingStrategy.sol deleted file mode 100644 index bb1609e..0000000 --- a/src/utils/RegistryPricingStrategy.sol +++ /dev/null @@ -1,4 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "slice/utils/hooks/RegistryPricingStrategy.sol"; diff --git a/src/utils/RegistryPricingStrategyAction.sol b/src/utils/RegistryPricingStrategyAction.sol deleted file mode 100644 index 09acaf8..0000000 --- a/src/utils/RegistryPricingStrategyAction.sol +++ /dev/null @@ -1,4 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "slice/utils/hooks/RegistryPricingStrategyAction.sol"; diff --git a/src/utils/RegistryProductAction.sol b/src/utils/RegistryProductAction.sol new file mode 100644 index 0000000..6b7f75a --- /dev/null +++ b/src/utils/RegistryProductAction.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "slice/utils/hooks/RegistryProductAction.sol"; diff --git a/src/utils/RegistryProductPrice.sol b/src/utils/RegistryProductPrice.sol new file mode 100644 index 0000000..be178fd --- /dev/null +++ b/src/utils/RegistryProductPrice.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "slice/utils/hooks/RegistryProductPrice.sol"; diff --git a/src/utils/RegistryProductPriceAction.sol b/src/utils/RegistryProductPriceAction.sol new file mode 100644 index 0000000..8196d88 --- /dev/null +++ b/src/utils/RegistryProductPriceAction.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "slice/utils/hooks/RegistryProductPriceAction.sol"; diff --git a/test/actions/Allowlisted/Allowlisted.t.sol b/test/actions/Allowlisted/Allowlisted.t.sol index 187618c..7082c5f 100644 --- a/test/actions/Allowlisted/Allowlisted.t.sol +++ b/test/actions/Allowlisted/Allowlisted.t.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {RegistryOnchainActionTest} from "@test/utils/RegistryOnchainActionTest.sol"; +import {RegistryProductActionTest} from "@test/utils/RegistryProductActionTest.sol"; import {Allowlisted} from "@/hooks/actions/Allowlisted/Allowlisted.sol"; import {Merkle} from "@murky/Merkle.sol"; uint256 constant slicerId = 0; uint256 constant productId = 1; -contract AllowlistedTest is RegistryOnchainActionTest { +contract AllowlistedTest is RegistryProductActionTest { Allowlisted allowlisted; Merkle m; bytes32[] data; diff --git a/test/actions/ERC20Gated/ERC20Gated.t.sol b/test/actions/ERC20Gated/ERC20Gated.t.sol index 4bd79f0..1806ab7 100644 --- a/test/actions/ERC20Gated/ERC20Gated.t.sol +++ b/test/actions/ERC20Gated/ERC20Gated.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {RegistryOnchainActionTest} from "@test/utils/RegistryOnchainActionTest.sol"; +import {RegistryProductActionTest} from "@test/utils/RegistryProductActionTest.sol"; import {MockERC20Gated} from "./mocks/MockERC20Gated.sol"; import {ERC20Gate} from "@/hooks/actions/ERC20Gated/ERC20Gated.sol"; import {IERC20, MockERC20} from "@test/utils/mocks/MockERC20.sol"; @@ -9,7 +9,7 @@ import {IERC20, MockERC20} from "@test/utils/mocks/MockERC20.sol"; uint256 constant slicerId = 0; uint256 constant productId = 1; -contract ERC20GatedTest is RegistryOnchainActionTest { +contract ERC20GatedTest is RegistryProductActionTest { MockERC20Gated erc20Gated; MockERC20 token = new MockERC20("Test", "TST", 18); MockERC20 token2 = new MockERC20("Test2", "TST2", 18); diff --git a/test/actions/ERC20Mint/ERC20Mint.t.sol b/test/actions/ERC20Mint/ERC20Mint.t.sol index 4d66433..e1e1fc4 100644 --- a/test/actions/ERC20Mint/ERC20Mint.t.sol +++ b/test/actions/ERC20Mint/ERC20Mint.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {RegistryOnchainAction, RegistryOnchainActionTest} from "@test/utils/RegistryOnchainActionTest.sol"; +import {RegistryProductAction, RegistryProductActionTest} from "@test/utils/RegistryProductActionTest.sol"; 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"; @@ -10,7 +10,7 @@ import {console2} from "forge-std/console2.sol"; uint256 constant slicerId = 0; -contract ERC20MintTest is RegistryOnchainActionTest { +contract ERC20MintTest is RegistryProductActionTest { ERC20Mint erc20Mint; uint256[] productIds = [1, 2, 3, 4]; @@ -346,7 +346,7 @@ contract ERC20MintTest is RegistryOnchainActionTest { assertEq(token.totalSupply(), 1000); // at max supply // This should revert (no tokens available) - vm.expectRevert(RegistryOnchainAction.NotAllowed.selector); + vm.expectRevert(RegistryProductAction.NotAllowed.selector); vm.prank(address(PRODUCTS_MODULE)); erc20Mint.onProductPurchase(slicerId, productIds[0], buyer, 1, "", ""); } diff --git a/test/actions/ERC721Mint/ERC721Mint.t.sol b/test/actions/ERC721Mint/ERC721Mint.t.sol index c040a75..cd0ae4a 100644 --- a/test/actions/ERC721Mint/ERC721Mint.t.sol +++ b/test/actions/ERC721Mint/ERC721Mint.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {RegistryOnchainAction, RegistryOnchainActionTest} from "@test/utils/RegistryOnchainActionTest.sol"; +import {RegistryProductAction, RegistryProductActionTest} from "@test/utils/RegistryProductActionTest.sol"; 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"; @@ -10,7 +10,7 @@ import {console2} from "forge-std/console2.sol"; uint256 constant slicerId = 0; -contract ERC721MintTest is RegistryOnchainActionTest { +contract ERC721MintTest is RegistryProductActionTest { ERC721Mint erc721Mint; uint256[] productIds = [1, 2, 3, 4]; diff --git a/test/actions/NFTGated/NFTGated.t.sol b/test/actions/NFTGated/NFTGated.t.sol index e771678..6e3134f 100644 --- a/test/actions/NFTGated/NFTGated.t.sol +++ b/test/actions/NFTGated/NFTGated.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {RegistryOnchainActionTest} from "@test/utils/RegistryOnchainActionTest.sol"; +import {RegistryProductActionTest} from "@test/utils/RegistryProductActionTest.sol"; import {MockNFTGated} from "./mocks/MockNFTGated.sol"; import {NFTGates, NFTGate, NftType} from "@/hooks/actions/NFTGated/NFTGated.sol"; import {MockERC721} from "@test/utils/mocks/MockERC721.sol"; @@ -11,7 +11,7 @@ import {console2} from "forge-std/console2.sol"; uint256 constant slicerId = 0; -contract NFTGatedTest is RegistryOnchainActionTest { +contract NFTGatedTest is RegistryProductActionTest { MockNFTGated nftGated; MockERC721 nft721 = new MockERC721(); MockERC1155 nft1155 = new MockERC1155(); diff --git a/test/pricingActions/FirstForFree/FirstForFree.t.sol b/test/pricingActions/FirstForFree/FirstForFree.t.sol index bf6436e..a9f3ed6 100644 --- a/test/pricingActions/FirstForFree/FirstForFree.t.sol +++ b/test/pricingActions/FirstForFree/FirstForFree.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {RegistryPricingStrategyActionTest} from "@test/utils/RegistryPricingStrategyActionTest.sol"; +import {RegistryProductPriceActionTest} from "@test/utils/RegistryProductPriceActionTest.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"; @@ -26,7 +26,7 @@ contract MockERC1155Token is ITokenERC1155 { } } -contract FirstForFreeTest is RegistryPricingStrategyActionTest { +contract FirstForFreeTest is RegistryProductPriceActionTest { FirstForFree firstForFree; MockERC721 mockERC721; MockERC1155Token mockERC1155; diff --git a/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol b/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol index 8291c78..6e3dd03 100644 --- a/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol +++ b/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {RegistryPricingStrategyTest} from "@test/utils/RegistryPricingStrategyTest.sol"; +import {RegistryProductPriceTest} from "@test/utils/RegistryProductPriceTest.sol"; import {console2} from "forge-std/console2.sol"; import { IProductsModule, @@ -19,7 +19,7 @@ uint256 constant productId = 1; uint80 constant percentDiscountOne = 1000; // 10% uint80 constant percentDiscountTwo = 2000; // 20% -contract NFTDiscountTest is RegistryPricingStrategyTest { +contract NFTDiscountTest is RegistryProductPriceTest { NFTDiscount erc721GatedDiscount; MockERC721 nftOne = new MockERC721(); MockERC721 nftTwo = new MockERC721(); diff --git a/test/pricingStrategies/VRGDA/LinearVRGDA.t.sol b/test/pricingStrategies/VRGDA/LinearVRGDA.t.sol index fb2a617..249bb26 100644 --- a/test/pricingStrategies/VRGDA/LinearVRGDA.t.sol +++ b/test/pricingStrategies/VRGDA/LinearVRGDA.t.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {RegistryPricingStrategyTest} from "@test/utils/RegistryPricingStrategyTest.sol"; +import {RegistryProductPriceTest} from "@test/utils/RegistryProductPriceTest.sol"; import {wadLn, toWadUnsafe, toDaysWadUnsafe, fromDaysWadUnsafe} from "@/utils/math/SignedWadMath.sol"; import "./mocks/MockLinearVRGDAPrices.sol"; -import {IProductsModule} from "@/utils/PricingStrategy.sol"; +import {IProductsModule} from "@/utils/ProductPrice.sol"; uint256 constant ONE_THOUSAND_YEARS = 356 days * 1000; @@ -18,7 +18,7 @@ uint128 constant min = 1e18; int256 constant priceDecayPercent = 0.31e18; int256 constant perTimeUnit = 2e18; -contract LinearVRGDATest is RegistryPricingStrategyTest { +contract LinearVRGDATest is RegistryProductPriceTest { MockLinearVRGDAPrices vrgda; function setUp() public { diff --git a/test/pricingStrategies/VRGDA/LogisticVRGDA.t.sol b/test/pricingStrategies/VRGDA/LogisticVRGDA.t.sol index ff6ecc6..ce4bb18 100644 --- a/test/pricingStrategies/VRGDA/LogisticVRGDA.t.sol +++ b/test/pricingStrategies/VRGDA/LogisticVRGDA.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {RegistryPricingStrategyTest} from "@test/utils/RegistryPricingStrategyTest.sol"; +import {RegistryProductPriceTest} from "@test/utils/RegistryProductPriceTest.sol"; import {unsafeDiv, wadLn, toWadUnsafe, toDaysWadUnsafe, fromDaysWadUnsafe} from "@/utils/math/SignedWadMath.sol"; import "./mocks/MockLogisticVRGDAPrices.sol"; @@ -20,7 +20,7 @@ int256 constant timeScale = 0.0023e18; int256 constant logisticLimitAdjusted = int256((MAX_SELLABLE + 1) * 2e18); int256 constant logisticLimitDoubled = int256((MAX_SELLABLE + 1e18) * 2e18); -contract LogisticVRGDATest is RegistryPricingStrategyTest { +contract LogisticVRGDATest is RegistryProductPriceTest { MockLogisticVRGDAPrices vrgda; function setUp() public { diff --git a/test/pricingStrategies/VRGDA/correctness/LinearVRGDACorrectness.t.sol b/test/pricingStrategies/VRGDA/correctness/LinearVRGDACorrectness.t.sol index 2dee0bd..5cb158c 100644 --- a/test/pricingStrategies/VRGDA/correctness/LinearVRGDACorrectness.t.sol +++ b/test/pricingStrategies/VRGDA/correctness/LinearVRGDACorrectness.t.sol @@ -3,12 +3,12 @@ pragma solidity ^0.8.20; import {console} from "forge-std/console.sol"; import {Vm} from "forge-std/Vm.sol"; -import {RegistryPricingStrategyTest} from "@test/utils/RegistryPricingStrategyTest.sol"; +import {RegistryProductPriceTest} from "@test/utils/RegistryProductPriceTest.sol"; import {wadLn, toWadUnsafe} from "@/utils/math/SignedWadMath.sol"; -import {IProductsModule} from "@/utils/PricingStrategy.sol"; +import {IProductsModule} from "@/utils/ProductPrice.sol"; import {MockLinearVRGDAPrices, LinearVRGDAParams} from "../mocks/MockLinearVRGDAPrices.sol"; -contract LinearVRGDACorrectnessTest is RegistryPricingStrategyTest { +contract LinearVRGDACorrectnessTest is RegistryProductPriceTest { // Sample parameters for differential fuzzing campaign. uint256 constant maxTimeframe = 356 days * 10; uint256 constant maxSellable = 10000; diff --git a/test/utils/HookRegistryTest.sol b/test/utils/HookRegistryTest.sol index 3c8013b..b73ad6e 100644 --- a/test/utils/HookRegistryTest.sol +++ b/test/utils/HookRegistryTest.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.20; import {HookTest} from "./HookTest.sol"; -import {IHookRegistry} from "@/utils/RegistryPricingStrategy.sol"; +import {IHookRegistry} from "@/utils/RegistryProductPrice.sol"; import {MockProductsModule} from "./mocks/MockProductsModule.sol"; -import {SliceContext} from "@/utils/RegistryOnchainAction.sol"; +import {SliceContext} from "@/utils/RegistryProductAction.sol"; abstract contract HookRegistryTest is HookTest { function testParamsSchema() public view { diff --git a/test/utils/HookTest.sol b/test/utils/HookTest.sol index 3021ab5..1bb36b2 100644 --- a/test/utils/HookTest.sol +++ b/test/utils/HookTest.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; -import {IProductsModule} from "@/utils/OnchainAction.sol"; +import {IProductsModule} from "@/utils/ProductAction.sol"; import {MockProductsModule} from "./mocks/MockProductsModule.sol"; abstract contract HookTest is Test { diff --git a/test/utils/OnchainActionTest.sol b/test/utils/OnchainActionTest.sol deleted file mode 100644 index b5df674..0000000 --- a/test/utils/OnchainActionTest.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {HookTest} from "./HookTest.sol"; -import {OnchainAction, IOnchainAction} from "@/utils/OnchainAction.sol"; - -abstract contract OnchainActionTest is HookTest { - function testSupportsInterface_OnchainAction() public view { - assertTrue(IOnchainAction(hook).supportsInterface(type(IOnchainAction).interfaceId)); - } - - function testRevert_onProductPurchase_NotPurchase() public { - vm.expectRevert(abi.encodeWithSelector(OnchainAction.NotPurchase.selector)); - IOnchainAction(hook).onProductPurchase(0, 0, address(0), 0, "", ""); - } - - function testRevert_onProductPurchase_WrongSlicer() public { - uint256 unauthorizedSlicer = OnchainAction(hook).ALLOWED_SLICER_ID() + 1; - - vm.expectRevert(abi.encodeWithSelector(OnchainAction.WrongSlicer.selector)); - vm.prank(address(PRODUCTS_MODULE)); - IOnchainAction(hook).onProductPurchase(unauthorizedSlicer, 0, address(0), 0, "", ""); - } -} diff --git a/test/utils/PricingStrategyActionTest.sol b/test/utils/PricingStrategyActionTest.sol deleted file mode 100644 index 67f3a04..0000000 --- a/test/utils/PricingStrategyActionTest.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {HookTest} from "./HookTest.sol"; -import { - OnchainAction, PricingStrategyAction, IOnchainAction, IPricingStrategy -} from "@/utils/PricingStrategyAction.sol"; - -abstract contract PricingStrategyActionTest is HookTest { - function testSupportsInterface_PricingStrategyAction() public view { - assertTrue(PricingStrategyAction(hook).supportsInterface(type(IOnchainAction).interfaceId)); - assertTrue(PricingStrategyAction(hook).supportsInterface(type(IPricingStrategy).interfaceId)); - } - - function testRevert_onProductPurchase_NotPurchase() public { - vm.expectRevert(abi.encodeWithSelector(OnchainAction.NotPurchase.selector)); - IOnchainAction(hook).onProductPurchase(0, 0, address(0), 0, "", ""); - } - - function testRevert_onProductPurchase_WrongSlicer() public { - uint256 unauthorizedSlicer = OnchainAction(hook).ALLOWED_SLICER_ID() + 1; - - vm.expectRevert(abi.encodeWithSelector(OnchainAction.WrongSlicer.selector)); - vm.prank(address(PRODUCTS_MODULE)); - IOnchainAction(hook).onProductPurchase(unauthorizedSlicer, 0, address(0), 0, "", ""); - } -} diff --git a/test/utils/PricingStrategyTest.sol b/test/utils/PricingStrategyTest.sol deleted file mode 100644 index ef08096..0000000 --- a/test/utils/PricingStrategyTest.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {HookTest} from "./HookTest.sol"; -import {IPricingStrategy} from "@/utils/PricingStrategy.sol"; - -abstract contract PricingStrategyTest is HookTest { - function testSupportsInterface_PricingStrategy() public view { - assertTrue(IPricingStrategy(hook).supportsInterface(type(IPricingStrategy).interfaceId)); - } -} diff --git a/test/utils/ProductActionTest.sol b/test/utils/ProductActionTest.sol new file mode 100644 index 0000000..e109c0b --- /dev/null +++ b/test/utils/ProductActionTest.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {HookTest} from "./HookTest.sol"; +import {ProductAction, IProductAction} from "@/utils/ProductAction.sol"; + +abstract contract ProductActionTest is HookTest { + function testSupportsInterface_ProductAction() public view { + assertTrue(IProductAction(hook).supportsInterface(type(IProductAction).interfaceId)); + } + + function testRevert_onProductPurchase_NotPurchase() public { + vm.expectRevert(abi.encodeWithSelector(ProductAction.NotPurchase.selector)); + IProductAction(hook).onProductPurchase(0, 0, address(0), 0, "", ""); + } + + function testRevert_onProductPurchase_WrongSlicer() public { + uint256 unauthorizedSlicer = ProductAction(hook).ALLOWED_SLICER_ID() + 1; + + vm.expectRevert(abi.encodeWithSelector(ProductAction.WrongSlicer.selector)); + vm.prank(address(PRODUCTS_MODULE)); + IProductAction(hook).onProductPurchase(unauthorizedSlicer, 0, address(0), 0, "", ""); + } +} diff --git a/test/utils/ProductPriceActionTest.sol b/test/utils/ProductPriceActionTest.sol new file mode 100644 index 0000000..e2c52a0 --- /dev/null +++ b/test/utils/ProductPriceActionTest.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {HookTest} from "./HookTest.sol"; +import {ProductAction, ProductPriceAction, IProductAction, IProductPrice} from "@/utils/ProductPriceAction.sol"; + +abstract contract ProductPriceActionTest is HookTest { + function testSupportsInterface_ProductPriceAction() public view { + assertTrue(ProductPriceAction(hook).supportsInterface(type(IProductAction).interfaceId)); + assertTrue(ProductPriceAction(hook).supportsInterface(type(IProductPrice).interfaceId)); + } + + function testRevert_onProductPurchase_NotPurchase() public { + vm.expectRevert(abi.encodeWithSelector(ProductAction.NotPurchase.selector)); + IProductAction(hook).onProductPurchase(0, 0, address(0), 0, "", ""); + } + + function testRevert_onProductPurchase_WrongSlicer() public { + uint256 unauthorizedSlicer = ProductAction(hook).ALLOWED_SLICER_ID() + 1; + + vm.expectRevert(abi.encodeWithSelector(ProductAction.WrongSlicer.selector)); + vm.prank(address(PRODUCTS_MODULE)); + IProductAction(hook).onProductPurchase(unauthorizedSlicer, 0, address(0), 0, "", ""); + } +} diff --git a/test/utils/ProductPriceTest.sol b/test/utils/ProductPriceTest.sol new file mode 100644 index 0000000..5246c5b --- /dev/null +++ b/test/utils/ProductPriceTest.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {HookTest} from "./HookTest.sol"; +import {IProductPrice} from "@/utils/ProductPrice.sol"; + +abstract contract ProductPriceTest is HookTest { + function testSupportsInterface_ProductPrice() public view { + assertTrue(IProductPrice(hook).supportsInterface(type(IProductPrice).interfaceId)); + } +} diff --git a/test/utils/RegistryOnchainActionTest.sol b/test/utils/RegistryOnchainActionTest.sol deleted file mode 100644 index 67988aa..0000000 --- a/test/utils/RegistryOnchainActionTest.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {HookRegistryTest} from "./HookRegistryTest.sol"; -import {RegistryOnchainAction, IHookRegistry, IOnchainAction} from "@/utils/RegistryOnchainAction.sol"; - -abstract contract RegistryOnchainActionTest is HookRegistryTest { - function testSupportsInterface_RegistryOnchainAction() public view { - assertTrue(IOnchainAction(hook).supportsInterface(type(IOnchainAction).interfaceId)); - assertTrue(IOnchainAction(hook).supportsInterface(type(IHookRegistry).interfaceId)); - } - - function testRevert_onProductPurchase_NotPurchase() public { - vm.expectRevert(abi.encodeWithSelector(RegistryOnchainAction.NotPurchase.selector)); - IOnchainAction(hook).onProductPurchase(0, 0, address(0), 0, "", ""); - } -} diff --git a/test/utils/RegistryPricingStrategyActionTest.sol b/test/utils/RegistryPricingStrategyActionTest.sol deleted file mode 100644 index adcbe06..0000000 --- a/test/utils/RegistryPricingStrategyActionTest.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {HookRegistryTest} from "./HookRegistryTest.sol"; -import { - RegistryOnchainAction, - IHookRegistry, - IOnchainAction, - IPricingStrategy -} from "@/utils/RegistryPricingStrategyAction.sol"; - -abstract contract RegistryPricingStrategyActionTest is HookRegistryTest { - function testSupportsInterface_RegistryPricingStrategyAction() public view { - assertTrue(IOnchainAction(hook).supportsInterface(type(IOnchainAction).interfaceId)); - assertTrue(IOnchainAction(hook).supportsInterface(type(IPricingStrategy).interfaceId)); - assertTrue(IOnchainAction(hook).supportsInterface(type(IHookRegistry).interfaceId)); - } - - function testRevert_onProductPurchase_NotPurchase() public { - vm.expectRevert(abi.encodeWithSelector(RegistryOnchainAction.NotPurchase.selector)); - IOnchainAction(hook).onProductPurchase(0, 0, address(0), 0, "", ""); - } -} diff --git a/test/utils/RegistryPricingStrategyTest.sol b/test/utils/RegistryPricingStrategyTest.sol deleted file mode 100644 index 8b83327..0000000 --- a/test/utils/RegistryPricingStrategyTest.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {HookRegistryTest} from "./HookRegistryTest.sol"; -import {IHookRegistry, IPricingStrategy} from "@/utils/RegistryPricingStrategy.sol"; - -abstract contract RegistryPricingStrategyTest is HookRegistryTest { - function testSupportsInterface_RegistryPricingStrategy() public view { - assertTrue(IPricingStrategy(hook).supportsInterface(type(IPricingStrategy).interfaceId)); - assertTrue(IPricingStrategy(hook).supportsInterface(type(IHookRegistry).interfaceId)); - } -} diff --git a/test/utils/RegistryProductActionTest.sol b/test/utils/RegistryProductActionTest.sol new file mode 100644 index 0000000..c678d4e --- /dev/null +++ b/test/utils/RegistryProductActionTest.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {HookRegistryTest} from "./HookRegistryTest.sol"; +import {RegistryProductAction, IHookRegistry, IProductAction} from "@/utils/RegistryProductAction.sol"; + +abstract contract RegistryProductActionTest is HookRegistryTest { + function testSupportsInterface_RegistryProductAction() public view { + assertTrue(IProductAction(hook).supportsInterface(type(IProductAction).interfaceId)); + assertTrue(IProductAction(hook).supportsInterface(type(IHookRegistry).interfaceId)); + } + + function testRevert_onProductPurchase_NotPurchase() public { + vm.expectRevert(abi.encodeWithSelector(RegistryProductAction.NotPurchase.selector)); + IProductAction(hook).onProductPurchase(0, 0, address(0), 0, "", ""); + } +} diff --git a/test/utils/RegistryProductPriceActionTest.sol b/test/utils/RegistryProductPriceActionTest.sol new file mode 100644 index 0000000..a4679a5 --- /dev/null +++ b/test/utils/RegistryProductPriceActionTest.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {HookRegistryTest} from "./HookRegistryTest.sol"; +import { + RegistryProductAction, IHookRegistry, IProductAction, IProductPrice +} from "@/utils/RegistryProductPriceAction.sol"; + +abstract contract RegistryProductPriceActionTest is HookRegistryTest { + 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)); + } + + function testRevert_onProductPurchase_NotPurchase() public { + vm.expectRevert(abi.encodeWithSelector(RegistryProductAction.NotPurchase.selector)); + IProductAction(hook).onProductPurchase(0, 0, address(0), 0, "", ""); + } +} diff --git a/test/utils/RegistryProductPriceTest.sol b/test/utils/RegistryProductPriceTest.sol new file mode 100644 index 0000000..d84f54b --- /dev/null +++ b/test/utils/RegistryProductPriceTest.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {HookRegistryTest} from "./HookRegistryTest.sol"; +import {IHookRegistry, IProductPrice} from "@/utils/RegistryProductPrice.sol"; + +abstract contract RegistryProductPriceTest is HookRegistryTest { + function testSupportsInterface_RegistryProductPrice() public view { + assertTrue(IProductPrice(hook).supportsInterface(type(IProductPrice).interfaceId)); + assertTrue(IProductPrice(hook).supportsInterface(type(IHookRegistry).interfaceId)); + } +} diff --git a/test/utils/mocks/MockProductsModule.sol b/test/utils/mocks/MockProductsModule.sol index 424348c..9dae500 100644 --- a/test/utils/mocks/MockProductsModule.sol +++ b/test/utils/mocks/MockProductsModule.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -// import {IProductsModule} from "@/utils/OnchainAction.sol"; +// import {IProductsModule} from "@/utils/ProductAction.sol"; import {Test} from "forge-std/Test.sol"; contract MockProductsModule is From e0793984dab2f90e981aeb8c451eae19a349e713 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 24 Aug 2025 00:37:11 +0200 Subject: [PATCH 18/24] contracts: deps --- foundry.toml | 4 ++-- soldeer.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/foundry.toml b/foundry.toml index 626889f..245dd2e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,7 +7,7 @@ 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"}] remappings = [ - "slice/=dependencies/slice-0.0.6/", + "slice/=dependencies/slice-0.0.7/", "@openzeppelin-4.8.0/=dependencies/@openzeppelin-contracts-4.8.0/", "@openzeppelin-upgradeable-4.8.0/=dependencies/@openzeppelin-contracts-upgradeable-4.8.0/", "@erc721a/=dependencies/erc721a-4.3.0/contracts/", @@ -46,7 +46,7 @@ remappings_regenerate = false [dependencies] murky = "0.1.0" -slice = "0.0.6" +slice = "0.0.7" forge-std = "1.9.7" "@openzeppelin-contracts" = "4.8.0" "@openzeppelin-contracts-upgradeable" = "4.8.0" diff --git a/soldeer.lock b/soldeer.lock index 9e86754..2515aa8 100644 --- a/soldeer.lock +++ b/soldeer.lock @@ -35,7 +35,7 @@ integrity = "a41bd6903adfe80291f7b20c0317368e517db10c302e82aa7dc53776f17811cd" [[dependencies]] name = "slice" -version = "0.0.5" -url = "https://soldeer-revisions.s3.amazonaws.com/slice/0_0_5_31-07-2025_17:05:17_src.zip" -checksum = "c27b2ae030af9a14ab8e443052f0e1f67076fdc314377a09c89293dc9f3d99ce" -integrity = "bdb01a680685add0d3e23470b672638222d0073a9241beeafe30de692ab12f28" +version = "0.0.7" +url = "https://soldeer-revisions.s3.amazonaws.com/slice/0_0_7_23-08-2025_22:32:41_src.zip" +checksum = "ecb67f517b379576a555ea72ecfa37485c84b865dec1383e8f494479866ae236" +integrity = "0df612cdda4d727850fc5aa534986d7d5e79c599728c61cd9883792b7fd43d0a" From ccb8d9a81a1016edae8d1f23e8a6a1a671627c79 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 24 Aug 2025 00:44:48 +0200 Subject: [PATCH 19/24] remove murky dependency --- foundry.toml | 2 - soldeer.lock | 7 - test/actions/Allowlisted/Allowlisted.t.sol | 2 +- test/utils/murky/Merkle.sol | 31 ++++ test/utils/murky/MurkyBase.sol | 199 +++++++++++++++++++++ 5 files changed, 231 insertions(+), 10 deletions(-) create mode 100644 test/utils/murky/Merkle.sol create mode 100644 test/utils/murky/MurkyBase.sol diff --git a/foundry.toml b/foundry.toml index 245dd2e..25a428f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -11,7 +11,6 @@ remappings = [ "@openzeppelin-4.8.0/=dependencies/@openzeppelin-contracts-4.8.0/", "@openzeppelin-upgradeable-4.8.0/=dependencies/@openzeppelin-contracts-upgradeable-4.8.0/", "@erc721a/=dependencies/erc721a-4.3.0/contracts/", - "@murky/=dependencies/murky-0.1.0/src/", "forge-std/=dependencies/forge-std-1.9.7/src/", "@test/=test/", "@/=src/" @@ -45,7 +44,6 @@ remappings_generate = false remappings_regenerate = false [dependencies] -murky = "0.1.0" slice = "0.0.7" forge-std = "1.9.7" "@openzeppelin-contracts" = "4.8.0" diff --git a/soldeer.lock b/soldeer.lock index 2515aa8..5690771 100644 --- a/soldeer.lock +++ b/soldeer.lock @@ -26,13 +26,6 @@ url = "https://soldeer-revisions.s3.amazonaws.com/forge-std/1_9_7_28-04-2025_15: checksum = "8d9e0a885fa8ee6429a4d344aeb6799119f6a94c7c4fe6f188df79b0dce294ba" integrity = "9e60fdba82bc374df80db7f2951faff6467b9091873004a3d314cf0c084b3c7d" -[[dependencies]] -name = "murky" -version = "0.1.0" -url = "https://soldeer-revisions.s3.amazonaws.com/murky/0_1_0_27-02-2025_09:52:15_murky.zip" -checksum = "44716641e084b50af27de35f0676706c7cd42b22b39a12f7136fda4156023a15" -integrity = "a41bd6903adfe80291f7b20c0317368e517db10c302e82aa7dc53776f17811cd" - [[dependencies]] name = "slice" version = "0.0.7" diff --git a/test/actions/Allowlisted/Allowlisted.t.sol b/test/actions/Allowlisted/Allowlisted.t.sol index 7082c5f..c8ea84d 100644 --- a/test/actions/Allowlisted/Allowlisted.t.sol +++ b/test/actions/Allowlisted/Allowlisted.t.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.20; import {RegistryProductActionTest} from "@test/utils/RegistryProductActionTest.sol"; +import {Merkle} from "@test/utils/murky/Merkle.sol"; import {Allowlisted} from "@/hooks/actions/Allowlisted/Allowlisted.sol"; -import {Merkle} from "@murky/Merkle.sol"; uint256 constant slicerId = 0; uint256 constant productId = 1; diff --git a/test/utils/murky/Merkle.sol b/test/utils/murky/Merkle.sol new file mode 100644 index 0000000..2b9bcc4 --- /dev/null +++ b/test/utils/murky/Merkle.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "./MurkyBase.sol"; + +/// @notice Nascent, simple, kinda efficient (and improving!) Merkle proof generator and verifier +/// @author dmfxyz +/// @dev Note Generic Merkle Tree +contract Merkle is MurkyBase { + /** + * + * HASHING FUNCTION * + * + */ + + /// ascending sort and concat prior to hashing + function hashLeafPairs(bytes32 left, bytes32 right) public pure override returns (bytes32 _hash) { + assembly { + switch lt(left, right) + case 0 { + mstore(0x0, right) + mstore(0x20, left) + } + default { + mstore(0x0, left) + mstore(0x20, right) + } + _hash := keccak256(0x0, 0x40) + } + } +} diff --git a/test/utils/murky/MurkyBase.sol b/test/utils/murky/MurkyBase.sol new file mode 100644 index 0000000..d9344e9 --- /dev/null +++ b/test/utils/murky/MurkyBase.sol @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +abstract contract MurkyBase { + /** + * + * CONSTRUCTOR * + * + */ + constructor() {} + + /** + * + * VIRTUAL HASHING FUNCTIONS * + * + */ + function hashLeafPairs(bytes32 left, bytes32 right) public pure virtual returns (bytes32 _hash); + + /** + * + * PROOF VERIFICATION * + * + */ + function verifyProof(bytes32 root, bytes32[] memory proof, bytes32 valueToProve) + external + pure + virtual + returns (bool) + { + // proof length must be less than max array size + bytes32 rollingHash = valueToProve; + uint256 length = proof.length; + unchecked { + for (uint256 i = 0; i < length; ++i) { + rollingHash = hashLeafPairs(rollingHash, proof[i]); + } + } + return root == rollingHash; + } + + /** + * + * PROOF GENERATION * + * + */ + function getRoot(bytes32[] memory data) public pure virtual returns (bytes32) { + require(data.length > 1, "won't generate root for single leaf"); + while (data.length > 1) { + data = hashLevel(data); + } + return data[0]; + } + + function getProof(bytes32[] memory data, uint256 node) public pure virtual returns (bytes32[] memory) { + require(data.length > 1, "won't generate proof for single leaf"); + // The size of the proof is equal to the ceiling of log2(numLeaves) + bytes32[] memory result = new bytes32[](log2ceilBitMagic(data.length)); + uint256 pos = 0; + + // Two overflow risks: node, pos + // node: max array size is 2**256-1. Largest index in the array will be 1 less than that. Also, + // for dynamic arrays, size is limited to 2**64-1 + // pos: pos is bounded by log2(data.length), which should be less than type(uint256).max + while (data.length > 1) { + unchecked { + if (node & 0x1 == 1) { + result[pos] = data[node - 1]; + } else if (node + 1 == data.length) { + result[pos] = bytes32(0); + } else { + result[pos] = data[node + 1]; + } + ++pos; + node /= 2; + } + data = hashLevel(data); + } + return result; + } + + ///@dev function is private to prevent unsafe data from being passed + function hashLevel(bytes32[] memory data) private pure returns (bytes32[] memory) { + bytes32[] memory result; + + // Function is private, and all internal callers check that data.length >=2. + // Underflow is not possible as lowest possible value for data/result index is 1 + // overflow should be safe as length is / 2 always. + unchecked { + uint256 length = data.length; + if (length & 0x1 == 1) { + result = new bytes32[](length / 2 + 1); + result[result.length - 1] = hashLeafPairs(data[length - 1], bytes32(0)); + } else { + result = new bytes32[](length / 2); + } + // pos is upper bounded by data.length / 2, so safe even if array is at max size + uint256 pos = 0; + for (uint256 i = 0; i < length - 1; i += 2) { + result[pos] = hashLeafPairs(data[i], data[i + 1]); + ++pos; + } + } + return result; + } + + /** + * + * MATH "LIBRARY" * + * + */ + + /// @dev Note that x is assumed > 0 + function log2ceil(uint256 x) public pure returns (uint256) { + uint256 ceil = 0; + uint256 pOf2; + // If x is a power of 2, then this function will return a ceiling + // that is 1 greater than the actual ceiling. So we need to check if + // x is a power of 2, and subtract one from ceil if so. + assembly { + // we check by seeing if x == (~x + 1) & x. This applies a mask + // to find the lowest set bit of x and then checks it for equality + // with x. If they are equal, then x is a power of 2. + + /* Example + x has single bit set + x := 0000_1000 + (~x + 1) = (1111_0111) + 1 = 1111_1000 + (1111_1000 & 0000_1000) = 0000_1000 == x + + x has multiple bits set + x := 1001_0010 + (~x + 1) = (0110_1101 + 1) = 0110_1110 + (0110_1110 & x) = 0000_0010 != x + */ + + // we do some assembly magic to treat the bool as an integer later on + pOf2 := eq(and(add(not(x), 1), x), x) + } + + // if x == type(uint256).max, than ceil is capped at 256 + // if x == 0, then pO2 == 0, so ceil won't underflow + unchecked { + while (x > 0) { + x >>= 1; + ceil++; + } + ceil -= pOf2; // see above + } + return ceil; + } + + /// Original bitmagic adapted from https://github.com/paulrberg/prb-math/blob/main/contracts/PRBMath.sol + /// @dev Note that x assumed > 1 + function log2ceilBitMagic(uint256 x) public pure returns (uint256) { + if (x <= 1) { + return 0; + } + uint256 msb = 0; + uint256 _x = x; + if (x >= 2 ** 128) { + x >>= 128; + msb += 128; + } + if (x >= 2 ** 64) { + x >>= 64; + msb += 64; + } + if (x >= 2 ** 32) { + x >>= 32; + msb += 32; + } + if (x >= 2 ** 16) { + x >>= 16; + msb += 16; + } + if (x >= 2 ** 8) { + x >>= 8; + msb += 8; + } + if (x >= 2 ** 4) { + x >>= 4; + msb += 4; + } + if (x >= 2 ** 2) { + x >>= 2; + msb += 2; + } + if (x >= 2 ** 1) { + msb += 1; + } + + uint256 lsb = (~_x + 1) & _x; + if ((lsb == _x) && (msb > 0)) { + return msb; + } else { + return msb + 1; + } + } +} From 6dfa06047a9fcec96e37a517285ae2d2b42a162b Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Sun, 24 Aug 2025 01:24:50 +0200 Subject: [PATCH 20/24] wip --- README.md | 195 +++++++++++------------------ src/examples/README.md | 139 +++++--------------- src/hooks/actions/README.md | 78 +++++------- src/hooks/pricing/README.md | 70 +++++------ src/hooks/pricingActions/README.md | 99 +++++---------- 5 files changed, 198 insertions(+), 383 deletions(-) diff --git a/README.md b/README.md index e1da422..cb67727 100644 --- a/README.md +++ b/README.md @@ -1,148 +1,103 @@ -# ▼ Slice Hooks - -Smart contracts for creating custom pricing strategies and onchain actions for [Slice](https://slice.so) products. - -Hooks enable dynamic pricing, purchase restrictions, rewards, integration with external protocols and other custom behaviors when products are bought. - -## Repository Structure - -``` -src/ -├── hooks/ # Reusable hooks with registry support -│ ├── actions/ # Onchain actions (gating, rewards, etc.) -│ ├── pricing/ # Pricing strategies (NFT discounts, VRGDA, etc.) -│ └── pricingActions/ # Combined pricing + action hooks -├── examples/ # Product-specific reference implementations -├── interfaces/ # Core hook interfaces -└── utils/ # Base contracts and utilities + bv# ▼ Slice Hooks + +Smart contracts for custom pricing and actions on [Slice](https://slice.so) products. + +## Overview + +```mermaid +graph LR + User[User] -->|Purchase| Slice[Slice Protocol] + Slice -->|1. Get Price| PP[Pricing Hook] + Slice -->|2. Execute| PA[Action Hook] + + PP -->|Dynamic pricing| Slice + PA -->|Custom logic| External[External Contracts] + + style PP fill:#e1f5fe + style PA fill:#fff3e0 + style Slice fill:#f3e5f5 ``` -## Core Concepts - -Hooks are built around three main interfaces: - -- **[`IProductAction`](./src/interfaces/IProductAction.sol)**: Execute custom logic during purchases (eligibility checks, rewards, etc.) -- **[`IProductPrice`](./src/interfaces/IProductPrice.sol)**: Calculate dynamic prices for products -- **[`IHookRegistry`](./src/interfaces/IHookRegistry.sol)**: Enable reusable hooks across multiple products with frontend integration - -Hooks can be: - -- **Product-specific**: Custom smart contracts tailored for individual products. These are integrated using the `custom` onchain action or pricing strategy in Slice. -- **Registry hooks**: Reusable contracts designed to support multiple products. Registries enable automatic integration with Slice clients. +Hooks enable: +- **Dynamic pricing** - VRGDA, tiered discounts, NFT-based pricing +- **Purchase actions** - Minting, rewards, gating, external integrations -See [Hook types](#hook-types) for more details. - -## Product Purchase Lifecycle - -Here's how hooks integrate into the product purchase flow: +## Architecture ``` - Checkout - │ - ▼ -┌─────────────────────┐ -│ Price Fetching │ ← `productPrice` called here -│ (before purchase) │ (IProductPrice) -└─────────────────────┘ - │ - ▼ -┌─────────────────────┐ -│ Purchase Execution │ ← `onProductPurchase` called here -│ (during purchase) │ (IProductAction) -└─────────────────────┘ - │ - ▼ -┌─────────────────────┐ -│ Purchase Complete │ -└─────────────────────┘ +┌──────────────────────────────────────┐ +│ Hook Types │ +├──────────────┬───────────────────────┤ +│ Registry │ Product-Specific │ +│ (Reusable) │ (One-off) │ +├──────────────┼───────────────────────┤ +│ ✓ Frontend │ ✗ Frontend │ +│ integration│ integration │ +│ ✓ Multi- │ ✓ Custom logic │ +│ product │ ✓ Simpler setup │ +└──────────────┴───────────────────────┘ ``` -**Pricing Strategies** are called during the price fetching phase to calculate price based on buyer and custom logic - -**Onchain Actions** are executed during the purchase transaction to: -- Validate purchase eligibility -- Execute custom logic (gating, minting, rewards, etc.) - -## Hook Types +## Quick Start ### Registry Hooks (Reusable) +Deploy once, use everywhere. Auto-integrated with Slice frontend. -Deploy once, use across multiple products with frontend integration: - -- **[Actions](./src/hooks/actions/)**: See available onchain actions and implementation guide -- **[Pricing](./src/hooks/pricing/)**: See available pricing strategies and implementation guide -- **[Pricing Actions](./src/hooks/pricingActions/)**: See combined pricing + action hooks +📁 **[Actions](./src/hooks/actions/)** - Gating, minting, rewards +📁 **[Pricing](./src/hooks/pricing/)** - VRGDA, discounts +📁 **[Combined](./src/hooks/pricingActions/)** - Price + action ### Product-Specific Hooks +Custom implementations for individual products. -Tailored implementations for individual products: - -- **[Examples](./src/examples/)**: See real-world implementations and creation guide - -## Base Contracts - -The base contracts in `src/utils` are designed to be inherited, providing essential building blocks for developing custom Slice hooks efficiently. +📁 **[Examples](./src/examples/)** - Reference implementations -### Registry (Reusable): +## Purchase Flow -- **`RegistryProductAction`**: Base for reusable onchain actions -- **`RegistryProductPrice`**: Base for reusable pricing strategies -- **`RegistryProductPriceAction`**: Base for reusable pricing + action hooks +``` + ┌────────────┐ + │ Checkout │ + └─────┬──────┘ + │ + ┌─────▼──────┐ + │ Get Price │◄── IProductPrice + └─────┬──────┘ + │ + ┌─────▼──────┐ + │ Purchase │◄── IProductAction + └─────┬──────┘ + │ + ┌─────▼──────┐ + │ Complete │ + └────────────┘ +``` -### Product-Specific +## Interfaces -- **`ProductAction`**: Base for product-specific onchain actions -- **`ProductPrice`**: Base for product-specific pricing strategies -- **`ProductPriceAction`**: Base for product-specific pricing + action hooks +```solidity +interface IProductPrice { + function productPrice(...) returns (uint256 ethPrice, uint256 currencyPrice); +} -## Quick Start - -- **For reusable actions**: See detailed guides in [`/src/hooks/actions`](./src/hooks/actions) -- **For reusable pricing strategies**: See detailed guides in [`/src/hooks/pricing`](./src/hooks/pricing) -- **For reusable pricing strategy actions**: See detailed guides in [`/src/hooks/pricingActions`](./src/hooks/pricingActions) -- **For product-specific hooks**: See implementation examples in [`/src/examples/`](./src/examples/) +interface IProductAction { + function isPurchaseAllowed(...) returns (bool); + function onProductPurchase(...) external payable; +} +``` ## Development ```bash -forge soldeer install # Install dependencies -forge test # Run tests -forge build # Build +forge soldeer install # Install dependencies +forge test # Run tests +./script/deploy.sh # Deploy contracts ``` Requires [Foundry](https://book.getfoundry.sh/getting-started/installation). -### Deployment - -To deploy hooks, use the deployment script: - -```bash -./script/deploy.sh -``` - -The script will present you with a list of available contracts to deploy. Select the contract you want to deploy and follow the prompts. - -### Testing - -When writing tests for your hooks, inherit from the appropriate base test contract: - -- **`RegistryProductActionTest`**: For testing `RegistryProductAction` contracts -- **`RegistryProductPriceTest`**: For testing `RegistryProductPrice` contracts -- **`RegistryProductPriceActionTest`**: For testing `RegistryProductPriceAction` contracts -- **`ProductActionTest`**: For testing `ProductAction` contracts -- **`ProductPriceTest`**: For testing `ProductPrice` contracts -- **`ProductPriceActionTest`**: For testing `ProductPriceAction` contracts - -Inheriting the appropriate test contract for your hook allows you to focus your tests solely on your custom hook logic. - ## Contributing -To contribute a new hook to this repository: - -1. **Choose the appropriate hook type** based on your needs (registry vs product-specific) -2. **Implement your hook** following the existing patterns in the codebase -3. **Write comprehensive tests** using the appropriate test base contract -4. **Add documentation** explaining your hook's purpose and usage -5. **Submit a pull request** against this repository - -Make sure your contribution follows the existing code style and includes proper documentation. \ No newline at end of file +1. Choose hook type (registry vs product-specific) +2. Inherit appropriate base contract +3. Write tests using base test contracts +4. Submit PR with documentation \ No newline at end of file diff --git a/src/examples/README.md b/src/examples/README.md index c42f2ef..ceb0b28 100644 --- a/src/examples/README.md +++ b/src/examples/README.md @@ -1,135 +1,60 @@ -# Example Implementations +# Product-Specific Examples -This folder contains product-specific smart contract implementations that demonstrate how to use Slice hooks for real-world use cases. These examples can be used as templates for creating your own custom implementations, without focusing on reusability and integration with Slice. - -## Key Interfaces - -**IProductPrice**: -```solidity -interface IProductPrice { - function productPrice( - uint256 slicerId, - uint256 productId, - address currency, - uint256 quantity, - address buyer, - bytes memory data - ) external view returns (uint256 ethPrice, uint256 currencyPrice); -} -``` - -**IProductAction**: -```solidity -interface IProductAction { - function isPurchaseAllowed( - uint256 slicerId, - uint256 productId, - address account, - uint256 quantity, - bytes memory slicerCustomData, - bytes memory buyerCustomData - ) external view returns (bool); - - function onProductPurchase( - uint256 slicerId, - uint256 productId, - address account, - uint256 quantity, - bytes memory slicerCustomData, - bytes memory buyerCustomData - ) external payable; -} -``` - -## Base Contracts - -- **ProductAction**: Add arbitrary requirements and/or custom logic after product purchase. -- **ProductPrice**: Customize product pricing logic. -- **ProductPriceAction**: Provide functionality of both Onchain Actions and Pricing Strategies - -## Key Differences from Registry Hooks - -Unlike the reusable hooks in `/hooks/`, these examples: -- Are tailored for specific products/projects -- Inherit directly from base contracts (`ProductAction`, `ProductPrice`) -- Don't implement `IHookRegistry` (not intended for Slice frontend integration) -- Serve as reference implementations and starting points +Reference implementations for custom product hooks without registry support. ## Available Examples -### Actions +| Example | Type | Description | +|---------|------|-------------| +| **[BaseCafe_2](./actions/BaseCafe_2.sol)** | Action | Mints NFT on every purchase | +| **[BaseGirlsScout](./actions/BaseGirlsScout.sol)** | Action | Mints Base Girls Scout NFTs | -- **[BaseCafe_2](./actions/BaseCafe_2.sol)**: Onchain action that mints an NFT to the buyer on every purchase. -- **[BaseGirlsScout](./actions/BaseGirlsScout.sol)**: Onchain action that mints Base Girls Scout NFTs to the buyer on every purchase. +## Key Differences from Registry Hooks -## Creating Custom Product-Specific Hooks +| Registry Hooks | Product-Specific | +|----------------|------------------| +| ✓ Reusable across products | ✗ Single product only | +| ✓ Frontend auto-integration | ✗ Manual integration | +| ✓ Parameter validation | ✗ Hardcoded config | +| ✗ More complex | ✓ Simpler setup | -### Onchain Action +## Creating Product-Specific Hooks -To create a custom product-specific onchain action: +### Action Example -1. **Inherit from ProductAction**: ```solidity import {ProductAction, IProductsModule} from "@/utils/ProductAction.sol"; contract MyProductAction is ProductAction { constructor(IProductsModule productsModule, uint256 slicerId) ProductAction(productsModule, slicerId) {} + + function _onProductPurchase(...) internal override { + // Custom logic - mint NFTs, track purchases, etc. + } } ``` -2. **Implement required functions**: -```solidity -function _onProductPurchase( - uint256 slicerId, - uint256 productId, - address account, - uint256 quantity, - bytes memory slicerCustomData, - bytes memory buyerCustomData -) internal override { - // Your custom logic here - mint NFTs, track purchases, etc. -} - -// Optional: Add purchase restrictions -function isPurchaseAllowed( - uint256 slicerId, - uint256 productId, - address account, - uint256 quantity, - bytes memory slicerCustomData, - bytes memory buyerCustomData -) public view override returns (bool) { - // Your eligibility logic here -} -``` - -### Pricing Strategy - -To create a custom product-specific pricing strategy: +### Pricing Example -1. **Inherit from ProductPrice**: ```solidity import {ProductPrice, IProductsModule} from "@/utils/ProductPrice.sol"; -contract MyProductAction is ProductPrice { +contract MyProductPrice is ProductPrice { constructor(IProductsModule productsModule, uint256 slicerId) ProductPrice(productsModule, slicerId) {} + + function productPrice(...) public view override + returns (uint256 ethPrice, uint256 currencyPrice) { + // Custom pricing logic + } } ``` -2. **Implement required functions**: -```solidity -function productPrice(...) public view override returns (uint256 ethPrice, uint256 currencyPrice) { - // Your pricing logic here -} -``` - -## Using These Examples - -These examples show common patterns for: -- Product-specific NFT minting -- Integration with external contracts -- Onchain rewards +## When to Use -Copy and modify these examples to create your own product-specific implementations. \ No newline at end of file +Choose product-specific hooks when: +- Building for a single product +- Need maximum customization +- Don't require frontend auto-integration +- Want simpler deployment \ No newline at end of file diff --git a/src/hooks/actions/README.md b/src/hooks/actions/README.md index f109ee7..41f972d 100644 --- a/src/hooks/actions/README.md +++ b/src/hooks/actions/README.md @@ -1,51 +1,21 @@ # Onchain Actions -Onchain actions are smart contracts that execute custom logic when products are purchased on Slice. They implement the `IProductAction` interface and can control purchase eligibility and perform actions after purchases. - -## Key Interface: IProductAction - -```solidity -interface IProductAction { - function isPurchaseAllowed( - uint256 slicerId, - uint256 productId, - address account, - uint256 quantity, - bytes memory slicerCustomData, - bytes memory buyerCustomData - ) external view returns (bool); - - function onProductPurchase( - uint256 slicerId, - uint256 productId, - address account, - uint256 quantity, - bytes memory slicerCustomData, - bytes memory buyerCustomData - ) external payable; -} -``` - -## Base Contract: RegistryProductAction - -All actions in this directory inherit from `RegistryProductAction`, which provides: -- Registry functionality for reusable hooks across multiple products -- Implementation of `IHookRegistry` for Slice frontend integration -- Base implementations for common patterns +Execute custom logic when products are purchased on Slice. ## Available Actions -- **[Allowlisted](./Allowlisted/Allowlisted.sol)**: Onchain action registry for allowlist requirement. -- **[ERC20Gated](./ERC20Gated/ERC20Gated.sol)**: Onchain action registry for ERC20 token gating. -- **[ERC20Mint](./ERC20Mint/ERC20Mint.sol)**: Onchain action registry that mints ERC20 tokens to buyers. -- **[ERC721AMint](./ERC721AMint/ERC721Mint.sol)**: Onchain action registry that mints ERC721A tokens to buyers. -- **[NFTGated](./NFTGated/NFTGated.sol)**: Onchain action registry for NFT gating. +| Action | Description | +|--------|-------------| +| **[Allowlisted](./Allowlisted/)** | Restrict purchases to allowlisted addresses | +| **[ERC20Gated](./ERC20Gated/)** | Require ERC20 token ownership | +| **[ERC20Mint](./ERC20Mint/)** | Mint ERC20 tokens to buyers | +| **[ERC721AMint](./ERC721AMint/)** | Mint ERC721A NFTs to buyers | +| **[NFTGated](./NFTGated/)** | Require NFT ownership | ## Creating Custom Actions -To create a custom onchain action: +### 1. Inherit Base Contract -1. **Inherit from RegistryProductAction**: ```solidity import {RegistryProductAction, IProductsModule} from "@/utils/RegistryProductAction.sol"; @@ -55,29 +25,39 @@ contract MyAction is RegistryProductAction { } ``` -2. **Implement required functions**: +### 2. Implement Core Functions + ```solidity +// Check purchase eligibility function isPurchaseAllowed(...) public view override returns (bool) { - // Your eligibility logic here + // Your logic } +// Execute on purchase function _onProductPurchase(...) internal override { - // Custom logic to execute on purchase + // Your logic } +// Configure product function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { - // Handle product configuration + // Store configuration } +// Define parameters function paramsSchema() external pure override returns (string memory) { - return "uint256 param1,address param2"; // Your parameter schema + return "uint256 param1,address param2"; } ``` -## Integration with Slice +## Testing + +Inherit from `RegistryProductActionTest` for testing: -Actions that inherit from `RegistryProductAction` are automatically compatible with Slice frontends through the `IHookRegistry` interface, enabling: -- Product configuration via `configureProduct()` -- Parameter validation via `paramsSchema()` -- Automatic discovery and integration \ No newline at end of file +```solidity +import {RegistryProductActionTest} from "@test/utils/RegistryProductActionTest.sol"; + +contract MyActionTest is RegistryProductActionTest { + // Your tests +} +``` \ No newline at end of file diff --git a/src/hooks/pricing/README.md b/src/hooks/pricing/README.md index 7bbf2ef..c06e9a6 100644 --- a/src/hooks/pricing/README.md +++ b/src/hooks/pricing/README.md @@ -1,69 +1,57 @@ # Pricing Strategies -Pricing strategies are smart contracts that calculate dynamic prices for products on Slice. They implement the `IProductPrice` interface to provide custom pricing logic based on arbitrary factors and conditions. - -## Key Interface: IProductPrice - -```solidity -interface IProductPrice { - function productPrice( - uint256 slicerId, - uint256 productId, - address currency, - uint256 quantity, - address buyer, - bytes memory data - ) external view returns (uint256 ethPrice, uint256 currencyPrice); -} -``` - -## Base Contract: RegistryProductPrice - -All pricing strategies in this directory inherit from `RegistryProductPrice`, which provides: -- Registry functionality for reusable pricing across multiple products -- Implementation of `IHookRegistry` for Slice frontend integration -- Base implementations for common patterns +Calculate dynamic prices for products on Slice. ## Available Strategies -- **[TieredDiscount](./TieredDiscount/TieredDiscount.sol)**: Tiered discounts based on asset ownership -- **[LinearVRGDAPrices](./VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol)**: VRGDA with a linear issuance curve - Price library with different params for each Slice product -- **[LogisticVRGDAPrices](./VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol)**: VRGDA with a logistic issuance curve - Price library with different params for each Slice product -- **[VRGDAPrices](./VRGDA/VRGDAPrices.sol)**: Variable Rate Gradual Dutch Auction +| Strategy | Description | +|----------|-------------| +| **[TieredDiscount](./TieredDiscount/)** | Tiered discounts based on asset ownership | +| **[LinearVRGDAPrices](./VRGDA/LinearVRGDAPrices/)** | Linear VRGDA curve | +| **[LogisticVRGDAPrices](./VRGDA/LogisticVRGDAPrices/)** | Logistic VRGDA curve | -## Creating Custom Pricing Strategies +## Creating Custom Pricing -To create a custom pricing strategy: +### 1. Inherit Base Contract -1. **Inherit from RegistryProductPrice**: ```solidity import {RegistryProductPrice, IProductsModule} from "@/utils/RegistryProductPrice.sol"; -contract MyProductPrice is RegistryProductPrice { +contract MyPricing is RegistryProductPrice { constructor(IProductsModule productsModule) RegistryProductPrice(productsModule) {} } ``` -2. **Implement required functions**: +### 2. Implement Core Functions + ```solidity -function productPrice(...) public view override returns (uint256 ethPrice, uint256 currencyPrice) { - // Your pricing logic here +// Calculate price +function productPrice(...) public view override + returns (uint256 ethPrice, uint256 currencyPrice) { + // Your pricing logic } +// Configure product function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { - // Handle product configuration + // Store configuration } +// Define parameters function paramsSchema() external pure override returns (string memory) { - return "uint256 basePrice,uint256 multiplier"; // Your parameter schema + return "uint256 basePrice,uint256 multiplier"; } ``` -## Integration with Slice +## Testing + +Inherit from `RegistryProductPriceTest` for testing: -Pricing strategies that inherit from `RegistryProductPrice` are automatically compatible with Slice frontends through the `IHookRegistry` interface, enabling: -- Product configuration via `configureProduct()` -- Parameter validation via `paramsSchema()` -- Automatic discovery and integration \ No newline at end of file +```solidity +import {RegistryProductPriceTest} from "@test/utils/RegistryProductPriceTest.sol"; + +contract MyPricingTest is RegistryProductPriceTest { + // Your tests +} +``` \ No newline at end of file diff --git a/src/hooks/pricingActions/README.md b/src/hooks/pricingActions/README.md index 9221111..bfd414e 100644 --- a/src/hooks/pricingActions/README.md +++ b/src/hooks/pricingActions/README.md @@ -1,98 +1,65 @@ -# Pricing Strategy Actions +# Pricing + Actions -Pricing strategy actions combine both pricing strategies and onchain actions in a single contract. They implement both `IProductPrice` and `IProductAction` interfaces, allowing them to calculate dynamic prices AND execute custom logic during purchases. +Combine dynamic pricing with onchain actions in a single contract. -## Key Interfaces +## Available Hooks -**IProductPrice**: -```solidity -interface IProductPrice { - function productPrice( - uint256 slicerId, - uint256 productId, - address currency, - uint256 quantity, - address buyer, - bytes memory data - ) external view returns (uint256 ethPrice, uint256 currencyPrice); -} -``` - -**IProductAction**: -```solidity -interface IProductAction { - function isPurchaseAllowed( - uint256 slicerId, - uint256 productId, - address account, - uint256 quantity, - bytes memory slicerCustomData, - bytes memory buyerCustomData - ) external view returns (bool); - - function onProductPurchase( - uint256 slicerId, - uint256 productId, - address account, - uint256 quantity, - bytes memory slicerCustomData, - bytes memory buyerCustomData - ) external payable; -} -``` - -## Base Contract: RegistryProductPriceAction +| Hook | Description | +|------|-------------| +| **[FirstForFree](./FirstForFree/)** | First purchase free based on conditions | -All pricing strategy actions inherit from `RegistryProductPriceAction`, which provides: -- Combined functionality of both pricing strategies and onchain actions -- Registry functionality for reusable hooks across multiple products -- Implementation of `IHookRegistry` for Slice frontend integration +## Creating Combined Hooks -## Available Pricing Strategy Actions +### 1. Inherit Base Contract -- **[FirstForFree](./FirstForFree/FirstForFree.sol)**: Discounts the first purchase of a product for free, based on conditions. - -## Creating Custom Pricing Strategy Actions - -To create a custom pricing strategy action: - -1. **Inherit from RegistryProductPriceAction**: ```solidity import {RegistryProductPriceAction, IProductsModule} from "@/utils/RegistryProductPriceAction.sol"; -contract MyProductPriceAction is RegistryProductPriceAction { +contract MyHook is RegistryProductPriceAction { constructor(IProductsModule productsModule) RegistryProductPriceAction(productsModule) {} } ``` -2. **Implement required functions**: +### 2. Implement All Functions + ```solidity -function productPrice(...) public view override returns (uint256 ethPrice, uint256 currencyPrice) { - // Your pricing logic here +// Pricing logic +function productPrice(...) public view override + returns (uint256 ethPrice, uint256 currencyPrice) { + // Calculate price } +// Purchase eligibility function isPurchaseAllowed(...) public view override returns (bool) { - // Your eligibility logic here + // Check eligibility } +// Purchase action function _onProductPurchase(...) internal override { - // Custom logic to execute on purchase + // Execute action } +// Configuration function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { - // Handle product configuration + // Store config } +// Parameters function paramsSchema() external pure override returns (string memory) { - return "uint256 param1,address param2"; // Your parameter schema + return "uint256 discount,address token"; } ``` -## Integration with Slice +## Testing -Pricing strategy actions that inherit from `RegistryProductPriceAction` are automatically compatible with Slice frontends through the `IHookRegistry` interface, enabling: -- Product configuration via `configureProduct()` -- Parameter validation via `paramsSchema()` -- Automatic discovery and integration \ No newline at end of file +Inherit from `RegistryProductPriceActionTest` for testing: + +```solidity +import {RegistryProductPriceActionTest} from "@test/utils/RegistryProductPriceActionTest.sol"; + +contract MyHookTest is RegistryProductPriceActionTest { + // Your tests +} +``` \ No newline at end of file From a503e8a9dd9e3391f93826029be46257bbd53b2a Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Wed, 27 Aug 2025 14:51:23 +0200 Subject: [PATCH 21/24] wip --- README.md | 221 ++++++++++++++++++++++++------------ src/hooks/actions/README.md | 98 ++++++++++++---- src/hooks/pricing/README.md | 116 +++++++++++++++---- 3 files changed, 317 insertions(+), 118 deletions(-) diff --git a/README.md b/README.md index cb67727..73aee2c 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,174 @@ - bv# ▼ Slice Hooks - -Smart contracts for custom pricing and actions on [Slice](https://slice.so) products. - -## Overview - -```mermaid -graph LR - User[User] -->|Purchase| Slice[Slice Protocol] - Slice -->|1. Get Price| PP[Pricing Hook] - Slice -->|2. Execute| PA[Action Hook] - - PP -->|Dynamic pricing| Slice - PA -->|Custom logic| External[External Contracts] - - style PP fill:#e1f5fe - style PA fill:#fff3e0 - style Slice fill:#f3e5f5 -``` +# ▼ Slice Hooks + +Smart contracts for creating custom pricing strategies and onchain actions for [Slice](https://slice.so) products. -Hooks enable: -- **Dynamic pricing** - VRGDA, tiered discounts, NFT-based pricing -- **Purchase actions** - Minting, rewards, gating, external integrations +Hooks enable dynamic pricing, purchase restrictions, rewards, integration with external protocols and other custom behaviors when products are bought. ## Architecture +### Core Interfaces + +Hooks are built around three main interfaces: + +**IProductPrice** - Calculate dynamic prices for products: +```solidity +function productPrice( + uint256 slicerId, + uint256 productId, + address currency, + uint256 quantity, + address buyer, + bytes memory data +) external view returns (uint256 ethPrice, uint256 currencyPrice); +``` + +**IProductAction** - Execute custom logic during purchases (eligibility checks, rewards, etc.): +```solidity +function isPurchaseAllowed(...) external view returns (bool); +function onProductPurchase(...) external payable; +``` + +**IHookRegistry** - Enable reusable hooks across multiple products with frontend integration: +```solidity +function configureProduct(uint256 slicerId, uint256 productId, bytes memory params) external; +function paramsSchema() external pure returns (string memory); +``` + +### Product Purchase Lifecycle + +Here's how hooks integrate into the product purchase flow: + ``` -┌──────────────────────────────────────┐ -│ Hook Types │ -├──────────────┬───────────────────────┤ -│ Registry │ Product-Specific │ -│ (Reusable) │ (One-off) │ -├──────────────┼───────────────────────┤ -│ ✓ Frontend │ ✗ Frontend │ -│ integration│ integration │ -│ ✓ Multi- │ ✓ Custom logic │ -│ product │ ✓ Simpler setup │ -└──────────────┴───────────────────────┘ + Checkout + │ + ▼ +┌─────────────────────┐ +│ Price Fetching │ ← `IProductPrice.productPrice` +│ (before purchase) │ +└─────────────────────┘ + │ + ▼ +┌─────────────────────┐ +│ Purchase Execution │ ← `IProductAction.onProductPurchase` +│ (during purchase) │ +└─────────────────────┘ + │ + ▼ +┌─────────────────────┐ +│ Purchase Complete │ +└─────────────────────┘ ``` -## Quick Start +**Pricing Strategies** are called during the price fetching phase to calculate price based on buyer and custom logic + +**Onchain Actions** are executed during the purchase transaction to: +- Validate purchase eligibility +- Execute custom logic (gating, minting, rewards, etc.) + +### Hook Types + +#### Registry Hooks (Reusable) + +Reusable contracts designed to support multiple products. Registries are automatically integrated with Slice clients. + +- **[Actions](./src/hooks/actions/)**: See available onchain actions and implementation guide +- **[Pricing](./src/hooks/pricing/)**: See available pricing strategies and implementation guide +- **[Pricing Actions](./src/hooks/pricingActions/)**: See combined pricing + action hooks + +#### Product-Specific Hooks + +Custom smart contracts tailored for individual products. These are integrated using the `custom` onchain action or pricing strategy in Slice. + +- **[Examples](./src/examples/)**: See real-world implementations and creation guide + +## Get Started + +### Repository Structure + +``` +src/ +├── hooks/ # Reusable hooks with registry support +│ ├── actions/ # Onchain actions (gating, rewards, etc.) +│ ├── pricing/ # Pricing strategies (NFT discounts, VRGDA, etc.) +│ └── pricingActions/ # Combined pricing + action hooks +├── examples/ # Product-specific reference implementations +├── interfaces/ # Core hook interfaces +└── utils/ # Base contracts and utilities +``` ### Registry Hooks (Reusable) -Deploy once, use everywhere. Auto-integrated with Slice frontend. +Deploy once, configure for multiple products via Slice frontend. -📁 **[Actions](./src/hooks/actions/)** - Gating, minting, rewards -📁 **[Pricing](./src/hooks/pricing/)** - VRGDA, discounts -📁 **[Combined](./src/hooks/pricingActions/)** - Price + action +📁 **[Actions](./src/hooks/actions/)** - Purchase restrictions and onchain effects + • Allowlisting, token gating, NFT minting, rewards + +📁 **[Pricing](./src/hooks/pricing/)** - Dynamic pricing strategies + • VRGDA curves, tiered discounts, conditional pricing + +📁 **[Combined](./src/hooks/pricingActions/)** - Pricing + actions in one contract + • Complex behaviors like "first purchase free" ### Product-Specific Hooks Custom implementations for individual products. -📁 **[Examples](./src/examples/)** - Reference implementations +📁 **[Examples](./src/examples/)** - Reference implementations + • Real-world patterns and starting templates -## Purchase Flow +## Base Contracts -``` - ┌────────────┐ - │ Checkout │ - └─────┬──────┘ - │ - ┌─────▼──────┐ - │ Get Price │◄── IProductPrice - └─────┬──────┘ - │ - ┌─────▼──────┐ - │ Purchase │◄── IProductAction - └─────┬──────┘ - │ - ┌─────▼──────┐ - │ Complete │ - └────────────┘ -``` +Located in `src/utils/`, these provide essential building blocks: -## Interfaces +**Registry bases** (for reusable hooks): +- `RegistryProductAction` - Reusable onchain actions +- `RegistryProductPrice` - Reusable pricing strategies +- `RegistryProductPriceAction` - Combined pricing + actions -```solidity -interface IProductPrice { - function productPrice(...) returns (uint256 ethPrice, uint256 currencyPrice); -} - -interface IProductAction { - function isPurchaseAllowed(...) returns (bool); - function onProductPurchase(...) external payable; -} -``` +**Product-specific bases** (for custom hooks): +- `ProductAction` - Single-product actions +- `ProductPrice` - Single-product pricing +- `ProductPriceAction` - Single-product combined ## Development +### Setup + ```bash -forge soldeer install # Install dependencies -forge test # Run tests -./script/deploy.sh # Deploy contracts +forge soldeer install # Install dependencies +forge build # Compile contracts +forge test # Run test suite ``` Requires [Foundry](https://book.getfoundry.sh/getting-started/installation). +### Deployment + +```bash +./script/deploy.sh # Interactive deployment script +``` + +The script presents available contracts and guides through deployment. + +### Testing + +Inherit from the appropriate test base: +- `RegistryProductActionTest` - Test registry actions +- `RegistryProductPriceTest` - Test registry pricing +- `RegistryProductPriceActionTest` - Test combined registry hooks +- `ProductActionTest` - Test product-specific actions +- `ProductPriceTest` - Test product-specific pricing +- `ProductPriceActionTest` - Test combined product hooks + ## Contributing -1. Choose hook type (registry vs product-specific) -2. Inherit appropriate base contract -3. Write tests using base test contracts -4. Submit PR with documentation \ No newline at end of file +1. **Choose hook type** - Registry (reusable) or product-specific +2. **Inherit base contract** - Use appropriate base from `src/utils/` +3. **Implement interfaces** - Follow patterns in existing hooks +4. **Write comprehensive tests** - Use test base contracts +5. **Document your hook** - Explain purpose, parameters, and usage +6. **Submit PR** - Include tests and documentation + +## Resources + +- [Slice Documentation](https://docs.slice.so) +- [Hook Integration Guide](https://docs.slice.so/hooks) +- [Example Implementations](./src/examples/) \ No newline at end of file diff --git a/src/hooks/actions/README.md b/src/hooks/actions/README.md index 41f972d..2a119dd 100644 --- a/src/hooks/actions/README.md +++ b/src/hooks/actions/README.md @@ -1,16 +1,23 @@ # Onchain Actions -Execute custom logic when products are purchased on Slice. +Execute custom logic when products are purchased on Slice. Actions implement the `IProductAction` interface to control purchase eligibility and perform operations during transactions. ## Available Actions -| Action | Description | -|--------|-------------| -| **[Allowlisted](./Allowlisted/)** | Restrict purchases to allowlisted addresses | -| **[ERC20Gated](./ERC20Gated/)** | Require ERC20 token ownership | -| **[ERC20Mint](./ERC20Mint/)** | Mint ERC20 tokens to buyers | -| **[ERC721AMint](./ERC721AMint/)** | Mint ERC721A NFTs to buyers | -| **[NFTGated](./NFTGated/)** | Require NFT ownership | +| Action | Description | Key Features | +|--------|-------------|--------------| +| **[Allowlisted](./Allowlisted/)** | Restrict purchases to allowlisted addresses | Merkle tree verification, gas-efficient | +| **[ERC20Gated](./ERC20Gated/)** | Require ERC20 token ownership | Minimum balance checks | +| **[ERC20Mint](./ERC20Mint/)** | Mint ERC20 tokens to buyers | Configurable amounts per purchase | +| **[ERC721AMint](./ERC721AMint/)** | Mint ERC721A NFTs efficiently | Gas-optimized batch minting | +| **[NFTGated](./NFTGated/)** | Require NFT ownership | ERC721/1155 support | + +## How Actions Work + +Actions are called at two points during purchase: + +1. **`isPurchaseAllowed`** - Before payment, checks if buyer can purchase +2. **`onProductPurchase`** - After payment, executes custom logic ## Creating Custom Actions @@ -28,36 +35,79 @@ contract MyAction is RegistryProductAction { ### 2. Implement Core Functions ```solidity -// Check purchase eligibility -function isPurchaseAllowed(...) public view override returns (bool) { - // Your logic +// Check if purchase is allowed (called before payment) +function isPurchaseAllowed( + uint256 slicerId, + uint256 productId, + address account, + uint256 quantity, + bytes memory slicerCustomData, + bytes memory buyerCustomData +) public view override returns (bool) { + // Your eligibility logic + // Example: check NFT ownership, allowlist, etc. + return isEligible; } -// Execute on purchase -function _onProductPurchase(...) internal override { - // Your logic +// Execute action (called after payment) +function _onProductPurchase( + uint256 slicerId, + uint256 productId, + address account, + uint256 quantity, + bytes memory slicerCustomData, + bytes memory buyerCustomData +) internal override { + // Your action logic + // Example: mint NFTs, distribute rewards, update state } -// Configure product -function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) - internal override { - // Store configuration +// Configure product (called when setting up) +function _configureProduct( + uint256 slicerId, + uint256 productId, + bytes memory params +) internal override { + // Decode and store configuration + // This enables reusability across products } -// Define parameters +// Define configuration parameters function paramsSchema() external pure override returns (string memory) { - return "uint256 param1,address param2"; + // Schema for frontend integration + return "address token,uint256 amount"; } ``` -## Testing +## Registry Integration -Inherit from `RegistryProductActionTest` for testing: +Actions inheriting from `RegistryProductAction` automatically support: +- **Product configuration** via `configureProduct()` +- **Parameter validation** via `paramsSchema()` +- **Frontend discovery** through `IHookRegistry` + +## Testing ```solidity import {RegistryProductActionTest} from "@test/utils/RegistryProductActionTest.sol"; contract MyActionTest is RegistryProductActionTest { - // Your tests + function setUp() public override { + super.setUp(); + // Your setup + } + + function test_myAction() public { + // Your tests + } } -``` \ No newline at end of file +``` + +## Best Practices + +- Keep `isPurchaseAllowed` gas-efficient (it's a view function) +- Validate all inputs in `_onProductPurchase` +- Use `slicerCustomData` for seller configuration +- Use `buyerCustomData` for buyer-specific parameters +- Emit events for important state changes +- Consider reentrancy protection if interacting with external contracts \ No newline at end of file diff --git a/src/hooks/pricing/README.md b/src/hooks/pricing/README.md index c06e9a6..0f38012 100644 --- a/src/hooks/pricing/README.md +++ b/src/hooks/pricing/README.md @@ -1,14 +1,27 @@ # Pricing Strategies -Calculate dynamic prices for products on Slice. +Calculate dynamic prices for products on Slice. Pricing strategies implement the `IProductPrice` interface to provide custom pricing logic based on various factors. ## Available Strategies -| Strategy | Description | -|----------|-------------| -| **[TieredDiscount](./TieredDiscount/)** | Tiered discounts based on asset ownership | -| **[LinearVRGDAPrices](./VRGDA/LinearVRGDAPrices/)** | Linear VRGDA curve | -| **[LogisticVRGDAPrices](./VRGDA/LogisticVRGDAPrices/)** | Logistic VRGDA curve | +| Strategy | Description | Use Cases | +|----------|-------------|-----------| +| **[TieredDiscount](./TieredDiscount/)** | Tiered discounts based on asset ownership | NFT holder discounts, token-based tiers | +| **[LinearVRGDAPrices](./VRGDA/LinearVRGDAPrices/)** | Linear VRGDA curve | Steady price increases over time | +| **[LogisticVRGDAPrices](./VRGDA/LogisticVRGDAPrices/)** | Logistic VRGDA curve | S-curve pricing, slow start/end | + +### VRGDA (Variable Rate Gradual Dutch Auction) + +VRGDA dynamically adjusts prices to maintain a target sales rate: +- Price increases when sales exceed target rate +- Price decreases when sales lag behind target +- Helps achieve predictable revenue over time + +## How Pricing Works + +The `productPrice` function is called before purchase to determine: +- **ETH price** - Price in native currency +- **Currency price** - Price in ERC20 tokens (if applicable) ## Creating Custom Pricing @@ -26,32 +39,97 @@ contract MyPricing is RegistryProductPrice { ### 2. Implement Core Functions ```solidity -// Calculate price -function productPrice(...) public view override - returns (uint256 ethPrice, uint256 currencyPrice) { +// Calculate product price +function productPrice( + uint256 slicerId, + uint256 productId, + address currency, + uint256 quantity, + address buyer, + bytes memory data +) public view override returns (uint256 ethPrice, uint256 currencyPrice) { // Your pricing logic + // Can factor in: + // - Buyer's token/NFT holdings + // - Time since launch + // - Total sales volume + // - Custom parameters + + uint256 basePrice = getBasePrice(slicerId, productId); + uint256 discount = calculateDiscount(buyer); + + ethPrice = (basePrice * quantity * (100 - discount)) / 100; + currencyPrice = 0; // Or calculate if accepting ERC20 } -// Configure product -function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) - internal override { - // Store configuration +// Configure product pricing +function _configureProduct( + uint256 slicerId, + uint256 productId, + bytes memory params +) internal override { + // Decode and store pricing parameters + (uint256 basePrice, uint256 discountRate) = abi.decode(params, (uint256, uint256)); + // Store configuration for this product } -// Define parameters +// Define configuration schema function paramsSchema() external pure override returns (string memory) { - return "uint256 basePrice,uint256 multiplier"; + // Schema for frontend integration + return "uint256 basePrice,uint256 discountRate"; } ``` -## Testing +## Advanced Patterns -Inherit from `RegistryProductPriceTest` for testing: +### Time-Based Pricing +```solidity +uint256 elapsed = block.timestamp - launchTime; +uint256 price = basePrice * (100 + elapsed / 1 days) / 100; +``` + +### Volume Discounts +```solidity +if (quantity >= 10) price = basePrice * 90 / 100; // 10% off +if (quantity >= 50) price = basePrice * 80 / 100; // 20% off +``` + +### NFT Holder Pricing +```solidity +if (IERC721(nftContract).balanceOf(buyer) > 0) { + price = basePrice * 50 / 100; // 50% discount +} +``` + +## Registry Integration + +Strategies inheriting from `RegistryProductPrice` automatically support: +- **Product configuration** via `configureProduct()` +- **Parameter validation** via `paramsSchema()` +- **Frontend discovery** through `IHookRegistry` + +## Testing ```solidity import {RegistryProductPriceTest} from "@test/utils/RegistryProductPriceTest.sol"; contract MyPricingTest is RegistryProductPriceTest { - // Your tests + function setUp() public override { + super.setUp(); + // Your setup + } + + function test_pricing() public { + // Test different pricing scenarios + } } -``` \ No newline at end of file +``` + +## Best Practices + +- Keep calculations gas-efficient (view function) +- Consider overflow/underflow protection +- Test edge cases (quantity = 0, max uint256) +- Document pricing algorithm clearly +- Emit events if storing state (in configure) +- Consider price stability mechanisms \ No newline at end of file From 7d04a41f7099e2290c86c8850e32d0813c47c506 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Thu, 28 Aug 2025 02:07:16 +0200 Subject: [PATCH 22/24] add generate-hook script --- README.md | 193 ++-- script/generate-hook.sh | 873 ++++++++++++++++++ src/hooks/actions/Allowlisted/Allowlisted.sol | 4 +- src/hooks/actions/ERC20Gated/ERC20Gated.sol | 16 +- src/hooks/actions/NFTGated/NFTGated.sol | 18 +- src/hooks/pricing/pricing.sol | 2 +- test/actions/ERC721Mint/ERC721Mint.t.sol | 2 +- .../TieredDiscount/NFTDiscount.t.sol | 1 - .../VRGDA/LinearVRGDA.t.sol | 0 .../VRGDA/LogisticVRGDA.t.sol | 0 .../correctness/LinearVRGDACorrectness.t.sol | 0 .../VRGDA/correctness/python/VRGDA.py | 0 .../VRGDA/correctness/python/compute_price.py | 0 .../VRGDA/correctness/python/requirements.txt | 0 .../VRGDA/mocks/MockLinearVRGDAPrices.sol | 0 .../VRGDA/mocks/MockLogisticVRGDAPrices.sol | 0 test/utils/HookTest.sol | 1 + test/utils/RegistryProductPriceActionTest.sol | 6 +- test/utils/RegistryProductPriceTest.sol | 2 +- 19 files changed, 988 insertions(+), 130 deletions(-) create mode 100755 script/generate-hook.sh rename test/{pricingStrategies => pricing}/TieredDiscount/NFTDiscount.t.sol (99%) rename test/{pricingStrategies => pricing}/VRGDA/LinearVRGDA.t.sol (100%) rename test/{pricingStrategies => pricing}/VRGDA/LogisticVRGDA.t.sol (100%) rename test/{pricingStrategies => pricing}/VRGDA/correctness/LinearVRGDACorrectness.t.sol (100%) rename test/{pricingStrategies => pricing}/VRGDA/correctness/python/VRGDA.py (100%) rename test/{pricingStrategies => pricing}/VRGDA/correctness/python/compute_price.py (100%) rename test/{pricingStrategies => pricing}/VRGDA/correctness/python/requirements.txt (100%) rename test/{pricingStrategies => pricing}/VRGDA/mocks/MockLinearVRGDAPrices.sol (100%) rename test/{pricingStrategies => pricing}/VRGDA/mocks/MockLogisticVRGDAPrices.sol (100%) diff --git a/README.md b/README.md index 73aee2c..01ab177 100644 --- a/README.md +++ b/README.md @@ -6,34 +6,6 @@ Hooks enable dynamic pricing, purchase restrictions, rewards, integration with e ## Architecture -### Core Interfaces - -Hooks are built around three main interfaces: - -**IProductPrice** - Calculate dynamic prices for products: -```solidity -function productPrice( - uint256 slicerId, - uint256 productId, - address currency, - uint256 quantity, - address buyer, - bytes memory data -) external view returns (uint256 ethPrice, uint256 currencyPrice); -``` - -**IProductAction** - Execute custom logic during purchases (eligibility checks, rewards, etc.): -```solidity -function isPurchaseAllowed(...) external view returns (bool); -function onProductPurchase(...) external payable; -``` - -**IHookRegistry** - Enable reusable hooks across multiple products with frontend integration: -```solidity -function configureProduct(uint256 slicerId, uint256 productId, bytes memory params) external; -function paramsSchema() external pure returns (string memory); -``` - ### Product Purchase Lifecycle Here's how hooks integrate into the product purchase flow: @@ -43,13 +15,13 @@ Here's how hooks integrate into the product purchase flow: │ ▼ ┌─────────────────────┐ -│ Price Fetching │ ← `IProductPrice.productPrice` -│ (before purchase) │ +│ Price Fetching │ ← `IProductPrice.productPrice()` +│ (before purchase) │ └─────────────────────┘ │ ▼ ┌─────────────────────┐ -│ Purchase Execution │ ← `IProductAction.onProductPurchase` +│ Purchase Execution │ ← `IProductAction.onProductPurchase()` │ (during purchase) │ └─────────────────────┘ │ @@ -65,72 +37,70 @@ Here's how hooks integrate into the product purchase flow: - Validate purchase eligibility - Execute custom logic (gating, minting, rewards, etc.) -### Hook Types - -#### Registry Hooks (Reusable) - -Reusable contracts designed to support multiple products. Registries are automatically integrated with Slice clients. - -- **[Actions](./src/hooks/actions/)**: See available onchain actions and implementation guide -- **[Pricing](./src/hooks/pricing/)**: See available pricing strategies and implementation guide -- **[Pricing Actions](./src/hooks/pricingActions/)**: See combined pricing + action hooks - -#### Product-Specific Hooks - -Custom smart contracts tailored for individual products. These are integrated using the `custom` onchain action or pricing strategy in Slice. - -- **[Examples](./src/examples/)**: See real-world implementations and creation guide - -## Get Started +### Core Interfaces -### Repository Structure +Hooks are built around three main interfaces: -``` -src/ -├── hooks/ # Reusable hooks with registry support -│ ├── actions/ # Onchain actions (gating, rewards, etc.) -│ ├── pricing/ # Pricing strategies (NFT discounts, VRGDA, etc.) -│ └── pricingActions/ # Combined pricing + action hooks -├── examples/ # Product-specific reference implementations -├── interfaces/ # Core hook interfaces -└── utils/ # Base contracts and utilities +**IProductPrice** - Calculate dynamic prices for products: +```solidity +function productPrice( + uint256 slicerId, + uint256 productId, + address currency, + uint256 quantity, + address buyer, + bytes memory data +) external view returns (uint256 ethPrice, uint256 currencyPrice); ``` -### Registry Hooks (Reusable) -Deploy once, configure for multiple products via Slice frontend. +**IProductAction** - Execute custom logic during purchases (eligibility checks, rewards, etc.): +```solidity +function isPurchaseAllowed( + uint256 slicerId, + uint256 productId, + address buyer, + uint256 quantity, + bytes memory slicerCustomData, + bytes memory buyerCustomData +) external view returns (bool); + +function onProductPurchase( + uint256 slicerId, + uint256 productId, + address buyer, + uint256 quantity, + bytes memory slicerCustomData, + bytes memory buyerCustomData +) external payable; +``` -📁 **[Actions](./src/hooks/actions/)** - Purchase restrictions and onchain effects - • Allowlisting, token gating, NFT minting, rewards +**IHookRegistry** - Enable reusable hooks across multiple products with frontend integration: +```solidity +function configureProduct(uint256 slicerId, uint256 productId, bytes memory params) external; +function paramsSchema() external pure returns (string memory); +``` -📁 **[Pricing](./src/hooks/pricing/)** - Dynamic pricing strategies - • VRGDA curves, tiered discounts, conditional pricing +### Hook Types -📁 **[Combined](./src/hooks/pricingActions/)** - Pricing + actions in one contract - • Complex behaviors like "first purchase free" +#### Registry Hooks (Reusable) -### Product-Specific Hooks -Custom implementations for individual products. +Reusable contracts designed to support multiple products, automatically integrated with Slice clients. -📁 **[Examples](./src/examples/)** - Reference implementations - • Real-world patterns and starting templates +- **[Actions](./src/hooks/actions/)** - Purchase restrictions and onchain effects +- **[Pricing](./src/hooks/pricing/)** - Dynamic pricing strategies +- **[PricingActions](./src/hooks/pricingActions/)** - Pricing + actions in one contract -## Base Contracts +#### Product-Specific Hooks -Located in `src/utils/`, these provide essential building blocks: +Custom smart contracts tailored for individual products, integrated using the `custom` onchain action or pricing strategy in Slice. -**Registry bases** (for reusable hooks): -- `RegistryProductAction` - Reusable onchain actions -- `RegistryProductPrice` - Reusable pricing strategies -- `RegistryProductPriceAction` - Combined pricing + actions +- **[Examples](./src/examples/)**: Reference implementations and templates -**Product-specific bases** (for custom hooks): -- `ProductAction` - Single-product actions -- `ProductPrice` - Single-product pricing -- `ProductPriceAction` - Single-product combined +All hooks inherit from base contracts in `src/utils/`. -## Development +## Contributing -### Setup +### Quick Start ```bash forge soldeer install # Install dependencies @@ -140,35 +110,50 @@ forge test # Run test suite Requires [Foundry](https://book.getfoundry.sh/getting-started/installation). -### Deployment +Deploy by running `./script/deploy.sh` and following instructions + +### Building a Hook + +The quickest way to create a new hook is using the interactive generator: ```bash -./script/deploy.sh # Interactive deployment script +./script/generate-hook.sh ``` -The script presents available contracts and guides through deployment. +This will guide you through: +1. Choosing hook scope (Registry or Product-specific) +2. Selecting hook type (Action, Pricing Strategy, or Pricing Action) +3. Naming your contract +4. Setting authorship (optional) -### Testing +The script automatically: +- Creates the contract file with appropriate template +- Adds imports to aggregator contracts (for registry hooks) +- Generates test files with proper structure (for registry hooks) -Inherit from the appropriate test base: -- `RegistryProductActionTest` - Test registry actions -- `RegistryProductPriceTest` - Test registry pricing -- `RegistryProductPriceActionTest` - Test combined registry hooks -- `ProductActionTest` - Test product-specific actions -- `ProductPriceTest` - Test product-specific pricing -- `ProductPriceActionTest` - Test combined product hooks +Once the hook is generated, add your custom contract logic to the and write tests for it. -## Contributing +For more detailed information, follow the appropriate guide for your hook type: +- [Actions](./src/hooks/actions/README.md) +- [Pricing](./src/hooks/pricing/README.md) +- [PricingActions](./src/hooks/pricingActions/README.md) + +### Repository Structure -1. **Choose hook type** - Registry (reusable) or product-specific -2. **Inherit base contract** - Use appropriate base from `src/utils/` -3. **Implement interfaces** - Follow patterns in existing hooks -4. **Write comprehensive tests** - Use test base contracts -5. **Document your hook** - Explain purpose, parameters, and usage -6. **Submit PR** - Include tests and documentation +``` +src/ +├── hooks/ # Reusable hooks with registry support +│ ├── actions/ # Onchain actions (gating, rewards, etc.) +│ ├── pricing/ # Pricing strategies (NFT discounts, VRGDA, etc.) +│ └── pricingActions/ # Combined pricing + action hooks +├── examples/ # Product-specific reference implementations +├── interfaces/ # Core hook interfaces +└── utils/ # Base contracts and utilities +``` -## Resources +### Resources -- [Slice Documentation](https://docs.slice.so) -- [Hook Integration Guide](https://docs.slice.so/hooks) -- [Example Implementations](./src/examples/) \ No newline at end of file +- [Actions Guide](./src/hooks/actions/README.md) +- [Pricing Strategies Guide](./src/hooks/pricing/README.md) +- [Pricing + Actions Guide](./src/hooks/pricingActions/README.md) +- [Example Implementations](./src/examples/README.md) \ No newline at end of file diff --git a/script/generate-hook.sh b/script/generate-hook.sh new file mode 100755 index 0000000..52dfb01 --- /dev/null +++ b/script/generate-hook.sh @@ -0,0 +1,873 @@ +#!/bin/bash + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${BLUE}=== Slice Hook Generator ===${NC}" +echo + +# Prompt for hook scope +echo -e "${YELLOW}Choose hook scope:${NC}" +echo "1) Registry (integrated, multi-product hook)" +echo "2) Product-specific (custom, single product hook)" +read -p "Enter your choice (1 or 2): " scope_choice + +case $scope_choice in + 1) + SCOPE="registry" + echo -e "${GREEN} Selected: Registry hook${NC}" + ;; + 2) + SCOPE="product" + echo -e "${GREEN} Selected: Product-specific hook${NC}" + ;; + *) + echo "Invalid choice. Exiting." + exit 1 + ;; +esac + +echo + +# Prompt for hook type +echo -e "${YELLOW}Choose hook type:${NC}" +echo "1) Onchain Action" +echo "2) Pricing Strategy" +echo "3) Pricing Action" +read -p "Enter your choice (1, 2, or 3): " type_choice + +case $type_choice in + 1) + TYPE="action" + TYPE_DISPLAY="Action" + echo -e "${GREEN} Selected: Onchain Action${NC}" + ;; + 2) + TYPE="pricing-strategy" + TYPE_DISPLAY="Pricing Strategy" + echo -e "${GREEN} Selected: Pricing Strategy${NC}" + ;; + 3) + TYPE="pricing-action" + TYPE_DISPLAY="Pricing Action" + echo -e "${GREEN} Selected: Pricing Action${NC}" + ;; + *) + echo "Invalid choice. Exiting." + exit 1 + ;; +esac + +echo + +# Prompt for contract name +read -p "Enter contract name (e.g., MyAction): " CONTRACT_NAME + +if [ -z "$CONTRACT_NAME" ]; then + echo "Contract name cannot be empty. Exiting." + exit 1 +fi + +# Capitalize first letter of contract name +CONTRACT_NAME=$(echo "$CONTRACT_NAME" | awk '{print toupper(substr($0,1,1)) substr($0,2)}') + +echo -e "${GREEN}✓ Contract name: ${CONTRACT_NAME}${NC}" + +# Check for duplicate hook names +EXISTING_DIRS="" +case $TYPE in + "action") + EXISTING_DIRS="src/hooks/actions/${CONTRACT_NAME}" + ;; + "pricing-strategy") + EXISTING_DIRS="src/hooks/pricing/${CONTRACT_NAME}" + ;; + "pricing-action") + EXISTING_DIRS="src/hooks/pricingActions/${CONTRACT_NAME}" + ;; +esac + +if [ -d "$EXISTING_DIRS" ]; then + echo -e "${RED}✗ Error: Hook '${CONTRACT_NAME}' already exists at ${EXISTING_DIRS}${NC}" + echo "Please choose a different name." + exit 1 +fi + +echo + +# Optional prompt for authorship +read -p "Enter author name (optional, press Enter to use 'Slice'): " AUTHOR +if [ -z "$AUTHOR" ]; then + AUTHOR="Slice" +fi +echo -e "${GREEN}✓ Author: ${AUTHOR}${NC}" +echo + +# Set directory based on type +case $TYPE in + "action") + DIR="src/hooks/actions/${CONTRACT_NAME}" + ;; + "pricing-strategy") + DIR="src/hooks/pricing/${CONTRACT_NAME}" + ;; + "pricing-action") + DIR="src/hooks/pricingActions/${CONTRACT_NAME}" + ;; +esac + +# Create directory +mkdir -p "$DIR" + +# Generate file path +FILE_PATH="${DIR}/${CONTRACT_NAME}.sol" + +echo -e "${BLUE}Generating contract at: ${FILE_PATH}${NC}" + +# Generate contract content based on scope and type +if [ "$SCOPE" = "registry" ] && [ "$TYPE" = "action" ]; then + cat > "$FILE_PATH" << 'EOF' +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { + RegistryProductAction, + HookRegistry, + IProductsModule, + IProductAction, + IHookRegistry +} from "@/utils/RegistryProductAction.sol"; + +/** + * @title CONTRACT_NAME + * @notice Onchain action registry contract. + * @author AUTHOR + */ +contract CONTRACT_NAME is RegistryProductAction { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(IProductsModule productsModuleAddress) RegistryProductAction(productsModuleAddress) {} + + /*////////////////////////////////////////////////////////////// + CONFIGURATION + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IProductAction + */ + function isPurchaseAllowed( + uint256 slicerId, + uint256 productId, + address account, + uint256 quantity, + bytes memory slicerCustomData, + bytes memory buyerCustomData + ) public view override returns (bool) { + // Your eligibility logic. Return true if eligible, false otherwise. + // Returns true by default. + + return true; + } + + /** + * @inheritdoc RegistryProductAction + */ + function _onProductPurchase( + uint256 slicerId, + uint256 productId, + address buyer, + uint256 quantity, + bytes memory slicerCustomData, + bytes memory buyerCustomData + ) internal override { + // Your logic to be executed after product purchase. + } + + /** + * @inheritdoc HookRegistry + */ + function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { + // Decode params according to `paramsSchema` and store any data required for your logic. + } + + /** + * @inheritdoc IHookRegistry + */ + function paramsSchema() external pure override returns (string memory) { + // Define the schema for the parameters that will be passed to `_configureProduct`. + return ""; + } +} +EOF + +elif [ "$SCOPE" = "product" ] && [ "$TYPE" = "action" ]; then + cat > "$FILE_PATH" << 'EOF' +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ProductAction, IProductsModule, IProductAction} from "@/utils/ProductAction.sol"; + +/** + * @title CONTRACT_NAME + * @notice Custom onchain action. + * @author AUTHOR + */ +contract CONTRACT_NAME is ProductAction { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(IProductsModule productsModuleAddress, uint256 slicerId) + ProductAction(productsModuleAddress, slicerId) + {} + + /*////////////////////////////////////////////////////////////// + CONFIGURATION + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IProductAction + */ + function isPurchaseAllowed( + uint256 slicerId, + uint256 productId, + address buyer, + uint256 quantity, + bytes memory slicerCustomData, + bytes memory buyerCustomData + ) public view override returns (bool) { + // Your eligibility logic. Return true if eligible, false otherwise. + return true; + } + + /** + * @inheritdoc ProductAction + */ + function _onProductPurchase( + uint256 slicerId, + uint256 productId, + address buyer, + uint256 quantity, + bytes memory slicerCustomData, + bytes memory buyerCustomData + ) internal override { + // Your logic to be executed after product purchase. + } +} +EOF + +elif [ "$SCOPE" = "registry" ] && [ "$TYPE" = "pricing-strategy" ]; then + cat > "$FILE_PATH" << 'EOF' +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { + RegistryProductPrice, + HookRegistry, + IProductsModule, + IProductPrice, + IHookRegistry +} from "@/utils/RegistryProductPrice.sol"; + +/** + * @title CONTRACT_NAME + * @notice Pricing strategy registry contract. + * @author AUTHOR + */ +contract CONTRACT_NAME is RegistryProductPrice { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(IProductsModule productsModuleAddress) RegistryProductPrice(productsModuleAddress) {} + + /*////////////////////////////////////////////////////////////// + CONFIGURATION + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IProductPrice + */ + function productPrice( + uint256 slicerId, + uint256 productId, + address currency, + uint256 quantity, + address buyer, + bytes memory data + ) public view override returns (uint256 ethPrice, uint256 currencyPrice) { + // Your pricing logic. Calculate and return the total price, depending on the passed quantity. + } + + /** + * @inheritdoc HookRegistry + */ + 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. + } + + /** + * @inheritdoc IHookRegistry + */ + function paramsSchema() external pure override returns (string memory) { + // Define the schema for the parameters that will be passed to `_configureProduct`. + return ""; + } +} +EOF + +elif [ "$SCOPE" = "product" ] && [ "$TYPE" = "pricing-strategy" ]; then + cat > "$FILE_PATH" << 'EOF' +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ProductPrice, IProductsModule, IProductPrice} from "@/utils/ProductPrice.sol"; + +/** + * @title CONTRACT_NAME + * @notice Custom pricing strategy. + * @author AUTHOR + */ +contract CONTRACT_NAME is ProductPrice { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(IProductsModule productsModuleAddress, uint256 slicerId) + ProductPrice(productsModuleAddress) + {} + + /*////////////////////////////////////////////////////////////// + CONFIGURATION + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IProductPrice + */ + function productPrice( + uint256 slicerId, + uint256 productId, + address currency, + uint256 quantity, + address buyer, + bytes memory data + ) external view returns (uint256 ethPrice, uint256 currencyPrice) { + // Your pricing logic. Calculate and return the total price. + } +} +EOF + +elif [ "$SCOPE" = "registry" ] && [ "$TYPE" = "pricing-action" ]; then + cat > "$FILE_PATH" << 'EOF' +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { + RegistryProductPriceAction, + RegistryProductAction, + HookRegistry, + IProductsModule, + IProductAction, + IProductPrice, + IHookRegistry +} from "@/utils/RegistryProductPriceAction.sol"; + +/** + * @title CONTRACT_NAME + * @notice Pricing action registry contract. + * @author AUTHOR + */ +contract CONTRACT_NAME is RegistryProductPriceAction { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(IProductsModule productsModuleAddress) RegistryProductPriceAction(productsModuleAddress) {} + + /*////////////////////////////////////////////////////////////// + CONFIGURATION + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IProductPrice + */ + function productPrice( + uint256 slicerId, + uint256 productId, + address currency, + uint256 quantity, + address buyer, + bytes memory data + ) public view override returns (uint256 ethPrice, uint256 currencyPrice) { + // Your pricing logic. Calculate and return the total price, depending on the passed quantity. + } + + /** + * @inheritdoc IProductAction + */ + function isPurchaseAllowed( + uint256 slicerId, + uint256 productId, + address account, + uint256 quantity, + bytes memory slicerCustomData, + bytes memory buyerCustomData + ) public view override returns (bool) { + // Your eligibility logic. Return true if eligible, false otherwise. + // Returns true by default. + + return true; + } + + /** + * @inheritdoc RegistryProductAction + */ + function _onProductPurchase( + uint256 slicerId, + uint256 productId, + address buyer, + uint256 quantity, + bytes memory slicerCustomData, + bytes memory buyerCustomData + ) internal override { + // Your logic to be executed after product purchase. + } + + /** + * @inheritdoc HookRegistry + */ + function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { + // Decode params according to `paramsSchema` and store any data required for your logic. + } + + /** + * @inheritdoc IHookRegistry + */ + function paramsSchema() external pure override returns (string memory) { + // Define the schema for the parameters that will be passed to `_configureProduct`. + return ""; + } +} +EOF + +elif [ "$SCOPE" = "product" ] && [ "$TYPE" = "pricing-action" ]; then + cat > "$FILE_PATH" << 'EOF' +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { + ProductPriceAction, + ProductAction, + IProductsModule, + IProductAction, + IProductPrice +} from "@/utils/ProductPriceAction.sol"; + +/** + * @title CONTRACT_NAME + * @notice Custom pricing action. + * @author AUTHOR + */ +contract CONTRACT_NAME is ProductPriceAction { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(IProductsModule productsModuleAddress, uint256 slicerId) + ProductPriceAction(productsModuleAddress, slicerId) + {} + + /*////////////////////////////////////////////////////////////// + CONFIGURATION + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IProductPrice + */ + function productPrice( + uint256 slicerId, + uint256 productId, + address currency, + uint256 quantity, + address buyer, + bytes memory data + ) external view returns (uint256 ethPrice, uint256 currencyPrice) { + // Your pricing logic. Calculate and return the total price. + } + + /** + * @inheritdoc IProductAction + */ + function isPurchaseAllowed( + uint256 slicerId, + uint256 productId, + address buyer, + uint256 quantity, + bytes memory slicerCustomData, + bytes memory buyerCustomData + ) public view override returns (bool) { + // Your eligibility logic. Return true if eligible, false otherwise. + return true; + } + + /** + * @inheritdoc ProductAction + */ + function _onProductPurchase( + uint256 slicerId, + uint256 productId, + address buyer, + uint256 quantity, + bytes memory slicerCustomData, + bytes memory buyerCustomData + ) internal override { + // Your logic to be executed after product purchase. + } +} +EOF + +else + echo "Error: Unsupported combination of scope and type." + exit 1 +fi + +# Replace placeholders with actual values +sed -i.bak "s/CONTRACT_NAME/${CONTRACT_NAME}/g" "$FILE_PATH" && rm "${FILE_PATH}.bak" +sed -i.bak "s/AUTHOR/${AUTHOR}/g" "$FILE_PATH" && rm "${FILE_PATH}.bak" + +echo -e "${GREEN}✅ Successfully generated ${CONTRACT_NAME}.sol${NC}" + +# Add to aggregator contract (only for registry hooks) +if [ "$SCOPE" = "registry" ]; then + AGGREGATOR_FILE="" + case $TYPE in + "action") + AGGREGATOR_FILE="src/hooks/actions/actions.sol" + ;; + "pricing-strategy") + AGGREGATOR_FILE="src/hooks/pricing/pricing.sol" + ;; + "pricing-action") + AGGREGATOR_FILE="src/hooks/pricingActions/pricingActions.sol" + ;; + esac + + if [ -f "$AGGREGATOR_FILE" ]; then + # Check if import already exists + if ! grep -q "import {${CONTRACT_NAME}}" "$AGGREGATOR_FILE"; then + # Construct the import path based on type + case $TYPE in + "action") + IMPORT_LINE="import {${CONTRACT_NAME}} from \"./${CONTRACT_NAME}/${CONTRACT_NAME}.sol\";" + ;; + "pricing-strategy") + IMPORT_LINE="import {${CONTRACT_NAME}} from \"./${CONTRACT_NAME}/${CONTRACT_NAME}.sol\";" + ;; + "pricing-action") + IMPORT_LINE="import {${CONTRACT_NAME}} from \"./${CONTRACT_NAME}/${CONTRACT_NAME}.sol\";" + ;; + esac + + # Find where to insert the import alphabetically + # Create temporary file with all imports including the new one + TEMP_FILE=$(mktemp) + + # Extract existing imports + grep "^import {" "$AGGREGATOR_FILE" > "$TEMP_FILE" + + # Add new import to temp file + echo "$IMPORT_LINE" >> "$TEMP_FILE" + + # Sort imports alphabetically + SORTED_IMPORTS=$(sort "$TEMP_FILE") + + # Find line number where imports start and end + FIRST_IMPORT=$(grep -n "^import {" "$AGGREGATOR_FILE" | head -1 | cut -d: -f1) + LAST_IMPORT=$(grep -n "^import {" "$AGGREGATOR_FILE" | tail -1 | cut -d: -f1) + + # Create new file with sorted imports + head -n $((FIRST_IMPORT - 1)) "$AGGREGATOR_FILE" > "${AGGREGATOR_FILE}.tmp" + echo "$SORTED_IMPORTS" >> "${AGGREGATOR_FILE}.tmp" + tail -n +$((LAST_IMPORT + 1)) "$AGGREGATOR_FILE" >> "${AGGREGATOR_FILE}.tmp" + + # Replace original file + mv "${AGGREGATOR_FILE}.tmp" "$AGGREGATOR_FILE" + + # Clean up temp file + rm "$TEMP_FILE" + + echo -e "${GREEN}✅ Added import to aggregator: ${AGGREGATOR_FILE}${NC}" + else + echo -e "${YELLOW}⚠️ Import already exists in aggregator${NC}" + fi + fi +fi + +# Generate test file for registry hooks +if [ "$SCOPE" = "registry" ]; then + TEST_DIR="" + case $TYPE in + "action") + TEST_DIR="test/actions/${CONTRACT_NAME}" + ;; + "pricing-strategy") + TEST_DIR="test/pricing/${CONTRACT_NAME}" + ;; + "pricing-action") + TEST_DIR="test/pricingActions/${CONTRACT_NAME}" + ;; + esac + + mkdir -p "$TEST_DIR" + TEST_FILE="${TEST_DIR}/${CONTRACT_NAME}.t.sol" + + echo -e "${BLUE}Generating test file at: ${TEST_FILE}${NC}" + + # Generate test content based on type + if [ "$TYPE" = "action" ]; then + cat > "$TEST_FILE" << 'EOF' +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {RegistryProductAction, RegistryProductActionTest} from "@test/utils/RegistryProductActionTest.sol"; +import {CONTRACT_NAME} from "@/hooks/actions/CONTRACT_NAME/CONTRACT_NAME.sol"; + +contract CONTRACT_NAMETest is RegistryProductActionTest { + CONTRACT_NAME CONTRACT_VAR; + uint256 slicerId = 1; + uint256 productId = 1; + + function setUp() public { + CONTRACT_VAR = new CONTRACT_NAME(PRODUCTS_MODULE); + _setHook(address(CONTRACT_VAR)); + } + + function testConfigureProduct() public { + vm.startPrank(productOwner); + + // Configure product + CONTRACT_VAR.configureProduct( + slicerId, + productId, + abi.encode( + // Your params here + ) + ); + + vm.stopPrank(); + + // Verify product is configured correctly + } + + function testIsPurchaseAllowed() public { + vm.startPrank(productOwner); + + // Configure product + CONTRACT_VAR.configureProduct( + slicerId, + productId, + abi.encode( + // Your params here + ) + ); + + vm.stopPrank(); + + bool isAllowed = CONTRACT_VAR.isPurchaseAllowed(slicerId, productId, buyer, 1, "", ""); + + // Verify isAllowed value based on conditions + } + + function testOnProductPurchase() public { + vm.startPrank(productOwner); + + // Configure product + CONTRACT_VAR.configureProduct( + slicerId, + productId, + abi.encode( + // Your params here + ) + ); + + vm.stopPrank(); + + vm.prank(address(PRODUCTS_MODULE)); + CONTRACT_VAR.onProductPurchase(slicerId, productId, buyer, 1, "", ""); + + // Verify after purchase logic + } +} +EOF + elif [ "$TYPE" = "pricing-strategy" ]; then + cat > "$TEST_FILE" << 'EOF' +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {RegistryProductPrice, RegistryProductPriceTest} from "@test/utils/RegistryProductPriceTest.sol"; +import {CONTRACT_NAME} from "@/hooks/pricing/CONTRACT_NAME/CONTRACT_NAME.sol"; + +contract CONTRACT_NAMETest is RegistryProductPriceTest { + CONTRACT_NAME CONTRACT_VAR; + uint256 slicerId = 1; + uint256 productId = 1; + + function setUp() public { + CONTRACT_VAR = new CONTRACT_NAME(PRODUCTS_MODULE); + _setHook(address(CONTRACT_VAR)); + } + + function testConfigureProduct() public { + vm.startPrank(productOwner); + + // Configure product + CONTRACT_VAR.configureProduct( + slicerId, + productId, + abi.encode( + // Your params here + ) + ); + + vm.stopPrank(); + + // Verify product is configured correctly + } + + function testProductPrice() public { + vm.startPrank(productOwner); + + // Configure product + CONTRACT_VAR.configureProduct( + slicerId, + productId, + abi.encode( + // Your params here + ) + ); + + vm.stopPrank(); + + (uint256 ethPrice, uint256 currencyPrice) = CONTRACT_VAR.productPrice(slicerId, productId, ETH, 1, buyer, ""); + + // Verify product price + } +} +EOF + elif [ "$TYPE" = "pricing-action" ]; then + cat > "$TEST_FILE" << 'EOF' +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {RegistryProductPriceAction, RegistryProductPriceActionTest} from "@test/utils/RegistryProductPriceActionTest.sol"; +import {CONTRACT_NAME} from "@/hooks/pricingActions/CONTRACT_NAME/CONTRACT_NAME.sol"; + +contract CONTRACT_NAMETest is RegistryProductPriceActionTest { + CONTRACT_NAME CONTRACT_VAR; + uint256 slicerId = 1; + uint256 productId = 1; + + function setUp() public { + CONTRACT_VAR = new CONTRACT_NAME(PRODUCTS_MODULE); + _setHook(address(CONTRACT_VAR)); + } + + function testConfigureProduct() public { + vm.startPrank(productOwner); + + // Configure product + CONTRACT_VAR.configureProduct( + slicerId, + productId, + abi.encode( + // Your params here + ) + ); + + vm.stopPrank(); + + // Verify product is configured correctly + } + + function testProductPrice() public { + vm.startPrank(productOwner); + + // Configure product + CONTRACT_VAR.configureProduct( + slicerId, + productId, + abi.encode( + // Your params here + ) + ); + + vm.stopPrank(); + + (uint256 ethPrice, uint256 currencyPrice) = CONTRACT_VAR.productPrice(slicerId, productId, ETH, 1, buyer, ""); + + // Verify product price + } + + function testIsPurchaseAllowed() public { + vm.startPrank(productOwner); + + // Configure product + CONTRACT_VAR.configureProduct( + slicerId, + productId, + abi.encode( + // Your params here + ) + ); + + vm.stopPrank(); + + bool isAllowed = CONTRACT_VAR.isPurchaseAllowed(slicerId, productId, buyer, 1, "", ""); + + // Verify isAllowed value based on conditions + } + + function testOnProductPurchase() public { + vm.startPrank(productOwner); + + // Configure product + CONTRACT_VAR.configureProduct( + slicerId, + productId, + abi.encode( + // Your params here + ) + ); + + vm.stopPrank(); + + vm.prank(address(PRODUCTS_MODULE)); + CONTRACT_VAR.onProductPurchase(slicerId, productId, buyer, 1, "", ""); + + // Verify after purchase logic + } +} +EOF + fi + + # Replace placeholders + CONTRACT_VAR=$(echo "$CONTRACT_NAME" | awk '{print tolower(substr($0,1,1)) substr($0,2)}') + sed -i.bak "s/CONTRACT_NAME/${CONTRACT_NAME}/g" "$TEST_FILE" && rm "${TEST_FILE}.bak" + sed -i.bak "s/CONTRACT_VAR/${CONTRACT_VAR}/g" "$TEST_FILE" && rm "${TEST_FILE}.bak" + + echo -e "${GREEN}✅ Generated test file: ${TEST_FILE}${NC}" +fi + +echo +echo -e "${YELLOW}Next steps:${NC}" +echo "1. Review and customize the generated contract" +echo "2. Implement your specific contract logic" +echo "3. Update the test file with your test cases" +echo "4. Run tests with 'forge test'" +echo "5. Deploy using the deployment scripts" \ No newline at end of file diff --git a/src/hooks/actions/Allowlisted/Allowlisted.sol b/src/hooks/actions/Allowlisted/Allowlisted.sol index 5240217..01e7811 100644 --- a/src/hooks/actions/Allowlisted/Allowlisted.sol +++ b/src/hooks/actions/Allowlisted/Allowlisted.sol @@ -39,7 +39,7 @@ contract Allowlisted is RegistryProductAction { function isPurchaseAllowed( uint256 slicerId, uint256 productId, - address account, + address buyer, uint256, bytes memory, bytes memory buyerCustomData @@ -47,7 +47,7 @@ contract Allowlisted is RegistryProductAction { // Get Merkle proof from buyerCustomData bytes32[] memory proof = abi.decode(buyerCustomData, (bytes32[])); - uint256 leafValue = uint256(uint160(account)); + uint256 leafValue = uint256(uint160(buyer)); // Generate leaf from account address bytes32 leaf; diff --git a/src/hooks/actions/ERC20Gated/ERC20Gated.sol b/src/hooks/actions/ERC20Gated/ERC20Gated.sol index 6a897ab..2072bce 100644 --- a/src/hooks/actions/ERC20Gated/ERC20Gated.sol +++ b/src/hooks/actions/ERC20Gated/ERC20Gated.sol @@ -36,19 +36,17 @@ 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 account, - uint256, - bytes memory, - bytes memory - ) public view override returns (bool) { + function isPurchaseAllowed(uint256 slicerId, uint256 productId, address buyer, uint256, bytes memory, bytes memory) + public + view + override + returns (bool) + { ERC20Gate[] memory gates = tokenGates[slicerId][productId]; for (uint256 i = 0; i < gates.length; i++) { ERC20Gate memory gate = gates[i]; - uint256 accountBalance = gate.erc20.balanceOf(account); + uint256 accountBalance = gate.erc20.balanceOf(buyer); if (accountBalance < gate.amount) { return false; } diff --git a/src/hooks/actions/NFTGated/NFTGated.sol b/src/hooks/actions/NFTGated/NFTGated.sol index 149c610..6301723 100644 --- a/src/hooks/actions/NFTGated/NFTGated.sol +++ b/src/hooks/actions/NFTGated/NFTGated.sol @@ -38,14 +38,12 @@ contract NFTGated is RegistryProductAction { * @inheritdoc IProductAction * @dev Checks if `account` owns the required amount of NFT tokens. */ - function isPurchaseAllowed( - uint256 slicerId, - uint256 productId, - address account, - uint256, - bytes memory, - bytes memory - ) public view override returns (bool isAllowed) { + function isPurchaseAllowed(uint256 slicerId, uint256 productId, address buyer, uint256, bytes memory, bytes memory) + public + view + override + returns (bool isAllowed) + { NFTGates memory nftGates_ = nftGates[slicerId][productId]; uint256 totalOwned; @@ -54,10 +52,10 @@ contract NFTGated is RegistryProductAction { NFTGate memory gate = nftGates_.gates[i]; if (gate.nftType == NftType.ERC1155) { - if (IERC1155(gate.nft).balanceOf(account, gate.id) >= gate.minQuantity) { + if (IERC1155(gate.nft).balanceOf(buyer, gate.id) >= gate.minQuantity) { ++totalOwned; } - } else if (IERC721(gate.nft).balanceOf(account) >= gate.minQuantity) { + } else if (IERC721(gate.nft).balanceOf(buyer) >= gate.minQuantity) { ++totalOwned; } diff --git a/src/hooks/pricing/pricing.sol b/src/hooks/pricing/pricing.sol index 7a35aa5..47b0454 100644 --- a/src/hooks/pricing/pricing.sol +++ b/src/hooks/pricing/pricing.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {NFTDiscount} from "./TieredDiscount/NFTDiscount/NFTDiscount.sol"; import {LinearVRGDAPrices} from "./VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol"; import {LogisticVRGDAPrices} from "./VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol"; +import {NFTDiscount} from "./TieredDiscount/NFTDiscount/NFTDiscount.sol"; diff --git a/test/actions/ERC721Mint/ERC721Mint.t.sol b/test/actions/ERC721Mint/ERC721Mint.t.sol index cd0ae4a..cee15be 100644 --- a/test/actions/ERC721Mint/ERC721Mint.t.sol +++ b/test/actions/ERC721Mint/ERC721Mint.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {RegistryProductAction, RegistryProductActionTest} from "@test/utils/RegistryProductActionTest.sol"; +import {RegistryProductActionTest} from "@test/utils/RegistryProductActionTest.sol"; 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"; diff --git a/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol b/test/pricing/TieredDiscount/NFTDiscount.t.sol similarity index 99% rename from test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol rename to test/pricing/TieredDiscount/NFTDiscount.t.sol index 6e3dd03..77d0a7e 100644 --- a/test/pricingStrategies/TieredDiscount/NFTDiscount.t.sol +++ b/test/pricing/TieredDiscount/NFTDiscount.t.sol @@ -12,7 +12,6 @@ import { import {MockERC721} from "@test/utils/mocks/MockERC721.sol"; import {MockERC1155} from "@test/utils/mocks/MockERC1155.sol"; -address constant ETH = address(0); address constant USDC = address(1); uint256 constant slicerId = 0; uint256 constant productId = 1; diff --git a/test/pricingStrategies/VRGDA/LinearVRGDA.t.sol b/test/pricing/VRGDA/LinearVRGDA.t.sol similarity index 100% rename from test/pricingStrategies/VRGDA/LinearVRGDA.t.sol rename to test/pricing/VRGDA/LinearVRGDA.t.sol diff --git a/test/pricingStrategies/VRGDA/LogisticVRGDA.t.sol b/test/pricing/VRGDA/LogisticVRGDA.t.sol similarity index 100% rename from test/pricingStrategies/VRGDA/LogisticVRGDA.t.sol rename to test/pricing/VRGDA/LogisticVRGDA.t.sol diff --git a/test/pricingStrategies/VRGDA/correctness/LinearVRGDACorrectness.t.sol b/test/pricing/VRGDA/correctness/LinearVRGDACorrectness.t.sol similarity index 100% rename from test/pricingStrategies/VRGDA/correctness/LinearVRGDACorrectness.t.sol rename to test/pricing/VRGDA/correctness/LinearVRGDACorrectness.t.sol diff --git a/test/pricingStrategies/VRGDA/correctness/python/VRGDA.py b/test/pricing/VRGDA/correctness/python/VRGDA.py similarity index 100% rename from test/pricingStrategies/VRGDA/correctness/python/VRGDA.py rename to test/pricing/VRGDA/correctness/python/VRGDA.py diff --git a/test/pricingStrategies/VRGDA/correctness/python/compute_price.py b/test/pricing/VRGDA/correctness/python/compute_price.py similarity index 100% rename from test/pricingStrategies/VRGDA/correctness/python/compute_price.py rename to test/pricing/VRGDA/correctness/python/compute_price.py diff --git a/test/pricingStrategies/VRGDA/correctness/python/requirements.txt b/test/pricing/VRGDA/correctness/python/requirements.txt similarity index 100% rename from test/pricingStrategies/VRGDA/correctness/python/requirements.txt rename to test/pricing/VRGDA/correctness/python/requirements.txt diff --git a/test/pricingStrategies/VRGDA/mocks/MockLinearVRGDAPrices.sol b/test/pricing/VRGDA/mocks/MockLinearVRGDAPrices.sol similarity index 100% rename from test/pricingStrategies/VRGDA/mocks/MockLinearVRGDAPrices.sol rename to test/pricing/VRGDA/mocks/MockLinearVRGDAPrices.sol diff --git a/test/pricingStrategies/VRGDA/mocks/MockLogisticVRGDAPrices.sol b/test/pricing/VRGDA/mocks/MockLogisticVRGDAPrices.sol similarity index 100% rename from test/pricingStrategies/VRGDA/mocks/MockLogisticVRGDAPrices.sol rename to test/pricing/VRGDA/mocks/MockLogisticVRGDAPrices.sol diff --git a/test/utils/HookTest.sol b/test/utils/HookTest.sol index 1bb36b2..0cf6ad7 100644 --- a/test/utils/HookTest.sol +++ b/test/utils/HookTest.sol @@ -8,6 +8,7 @@ import {MockProductsModule} from "./mocks/MockProductsModule.sol"; abstract contract HookTest is Test { IProductsModule public PRODUCTS_MODULE = IProductsModule(address(new MockProductsModule())); + address constant ETH = address(0); address public productOwner = makeAddr("productOwner"); address public buyer = makeAddr("buyer"); address public buyer2 = makeAddr("buyer2"); diff --git a/test/utils/RegistryProductPriceActionTest.sol b/test/utils/RegistryProductPriceActionTest.sol index a4679a5..38e19b6 100644 --- a/test/utils/RegistryProductPriceActionTest.sol +++ b/test/utils/RegistryProductPriceActionTest.sol @@ -3,7 +3,11 @@ pragma solidity ^0.8.20; import {HookRegistryTest} from "./HookRegistryTest.sol"; import { - RegistryProductAction, IHookRegistry, IProductAction, IProductPrice + RegistryProductPriceAction, + RegistryProductAction, + IHookRegistry, + IProductAction, + IProductPrice } from "@/utils/RegistryProductPriceAction.sol"; abstract contract RegistryProductPriceActionTest is HookRegistryTest { diff --git a/test/utils/RegistryProductPriceTest.sol b/test/utils/RegistryProductPriceTest.sol index d84f54b..1e93bae 100644 --- a/test/utils/RegistryProductPriceTest.sol +++ b/test/utils/RegistryProductPriceTest.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; import {HookRegistryTest} from "./HookRegistryTest.sol"; -import {IHookRegistry, IProductPrice} from "@/utils/RegistryProductPrice.sol"; +import {RegistryProductPrice, IHookRegistry, IProductPrice} from "@/utils/RegistryProductPrice.sol"; abstract contract RegistryProductPriceTest is HookRegistryTest { function testSupportsInterface_RegistryProductPrice() public view { From 2cffd877262725e6423f1a1ed94de6876cc4c937 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Thu, 28 Aug 2025 02:53:55 +0200 Subject: [PATCH 23/24] updated readme --- src/examples/README.md | 112 +++++++++++++++++++------ src/hooks/actions/README.md | 109 +++++-------------------- src/hooks/pricing/README.md | 126 ++++------------------------- src/hooks/pricingActions/README.md | 76 +++++++---------- 4 files changed, 150 insertions(+), 273 deletions(-) diff --git a/src/examples/README.md b/src/examples/README.md index ceb0b28..5a75896 100644 --- a/src/examples/README.md +++ b/src/examples/README.md @@ -2,12 +2,13 @@ Reference implementations for custom product hooks without registry support. -## Available Examples +## When to Use -| Example | Type | Description | -|---------|------|-------------| -| **[BaseCafe_2](./actions/BaseCafe_2.sol)** | Action | Mints NFT on every purchase | -| **[BaseGirlsScout](./actions/BaseGirlsScout.sol)** | Action | Mints Base Girls Scout NFTs | +Choose product-specific hooks when: +- Building for a single product +- Don't want others to use the same hook for their products +- Don't require client integration +- Need immediate deployment ## Key Differences from Registry Hooks @@ -20,17 +21,66 @@ Reference implementations for custom product hooks without registry support. ## Creating Product-Specific Hooks +### Quick Start with Generator Script + +The easiest way to create a new product-specific hook is using the hook generator: + +```bash +# From the hooks directory +./script/generate-hook.sh +``` + +Select: +1. Product-specific +2. The desired hook type (Action, Pricing, PricingAction) +3. Enter your contract name +4. Enter author name (optional) + ### Action Example ```solidity -import {ProductAction, IProductsModule} from "@/utils/ProductAction.sol"; +import {ProductAction, IProductsModule, IProductAction} from "@/utils/ProductAction.sol"; contract MyProductAction is ProductAction { - constructor(IProductsModule productsModule, uint256 slicerId) - ProductAction(productsModule, slicerId) {} - - function _onProductPurchase(...) internal override { - // Custom logic - mint NFTs, track purchases, etc. + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(IProductsModule productsModuleAddress, uint256 slicerId) + ProductAction(productsModuleAddress, slicerId) + {} + + /*////////////////////////////////////////////////////////////// + CONFIGURATION + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IProductAction + */ + function isPurchaseAllowed( + uint256 slicerId, + uint256 productId, + address buyer, + uint256 quantity, + bytes memory slicerCustomData, + bytes memory buyerCustomData + ) public view override returns (bool) { + // Your eligibility logic. Return true if eligible, false otherwise. + return true; + } + + /** + * @inheritdoc ProductAction + */ + function _onProductPurchase( + uint256 slicerId, + uint256 productId, + address buyer, + uint256 quantity, + bytes memory slicerCustomData, + bytes memory buyerCustomData + ) internal override { + // Your logic to be executed after product purchase. } } ``` @@ -38,23 +88,33 @@ contract MyProductAction is ProductAction { ### Pricing Example ```solidity -import {ProductPrice, IProductsModule} from "@/utils/ProductPrice.sol"; +import {ProductPrice, IProductsModule, IProductPrice} from "@/utils/ProductPrice.sol"; contract MyProductPrice is ProductPrice { - constructor(IProductsModule productsModule, uint256 slicerId) - ProductPrice(productsModule, slicerId) {} - - function productPrice(...) public view override - returns (uint256 ethPrice, uint256 currencyPrice) { - // Custom pricing logic + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(IProductsModule productsModuleAddress, uint256 slicerId) + ProductPrice(productsModuleAddress) + {} + + /*////////////////////////////////////////////////////////////// + CONFIGURATION + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IProductPrice + */ + function productPrice( + uint256 slicerId, + uint256 productId, + address currency, + uint256 quantity, + address buyer, + bytes memory data + ) external view returns (uint256 ethPrice, uint256 currencyPrice) { + // Your pricing logic. Calculate and return the total price. } } ``` - -## When to Use - -Choose product-specific hooks when: -- Building for a single product -- Need maximum customization -- Don't require frontend auto-integration -- Want simpler deployment \ No newline at end of file diff --git a/src/hooks/actions/README.md b/src/hooks/actions/README.md index 2a119dd..4770360 100644 --- a/src/hooks/actions/README.md +++ b/src/hooks/actions/README.md @@ -2,16 +2,6 @@ Execute custom logic when products are purchased on Slice. Actions implement the `IProductAction` interface to control purchase eligibility and perform operations during transactions. -## Available Actions - -| Action | Description | Key Features | -|--------|-------------|--------------| -| **[Allowlisted](./Allowlisted/)** | Restrict purchases to allowlisted addresses | Merkle tree verification, gas-efficient | -| **[ERC20Gated](./ERC20Gated/)** | Require ERC20 token ownership | Minimum balance checks | -| **[ERC20Mint](./ERC20Mint/)** | Mint ERC20 tokens to buyers | Configurable amounts per purchase | -| **[ERC721AMint](./ERC721AMint/)** | Mint ERC721A NFTs efficiently | Gas-optimized batch minting | -| **[NFTGated](./NFTGated/)** | Require NFT ownership | ERC721/1155 support | - ## How Actions Work Actions are called at two points during purchase: @@ -21,93 +11,34 @@ Actions are called at two points during purchase: ## Creating Custom Actions -### 1. Inherit Base Contract +### Quick Start with Generator Script -```solidity -import {RegistryProductAction, IProductsModule} from "@/utils/RegistryProductAction.sol"; +The easiest way to create a new action is using the hook generator: -contract MyAction is RegistryProductAction { - constructor(IProductsModule productsModule) - RegistryProductAction(productsModule) {} -} +```bash +# From the hooks directory +./script/generate-hook.sh ``` -### 2. Implement Core Functions - -```solidity -// Check if purchase is allowed (called before payment) -function isPurchaseAllowed( - uint256 slicerId, - uint256 productId, - address account, - uint256 quantity, - bytes memory slicerCustomData, - bytes memory buyerCustomData -) public view override returns (bool) { - // Your eligibility logic - // Example: check NFT ownership, allowlist, etc. - return isEligible; -} - -// Execute action (called after payment) -function _onProductPurchase( - uint256 slicerId, - uint256 productId, - address account, - uint256 quantity, - bytes memory slicerCustomData, - bytes memory buyerCustomData -) internal override { - // Your action logic - // Example: mint NFTs, distribute rewards, update state -} - -// Configure product (called when setting up) -function _configureProduct( - uint256 slicerId, - uint256 productId, - bytes memory params -) internal override { - // Decode and store configuration - // This enables reusability across products -} - -// Define configuration parameters -function paramsSchema() external pure override returns (string memory) { - // Schema for frontend integration - return "address token,uint256 amount"; -} -``` +Select: +1. Registry (for Slice-integrated hooks) +2. Onchain Action +3. Enter your contract name +4. Enter author name (optional) + +The script will create your contract file with the proper template and add it to the aggregator. -## Registry Integration +### Registry Integration -Actions inheriting from `RegistryProductAction` automatically support: +Actions inheriting from `RegistryProductAction` automatically support frontend integration through: - **Product configuration** via `configureProduct()` - **Parameter validation** via `paramsSchema()` -- **Frontend discovery** through `IHookRegistry` - -## Testing - -```solidity -import {RegistryProductActionTest} from "@test/utils/RegistryProductActionTest.sol"; - -contract MyActionTest is RegistryProductActionTest { - function setUp() public override { - super.setUp(); - // Your setup - } - - function test_myAction() public { - // Your tests - } -} -``` + +### Testing + +The generator script will also create a test file for your action. Customize it to your needs to test your action. ## Best Practices -- Keep `isPurchaseAllowed` gas-efficient (it's a view function) -- Validate all inputs in `_onProductPurchase` -- Use `slicerCustomData` for seller configuration -- Use `buyerCustomData` for buyer-specific parameters -- Emit events for important state changes -- Consider reentrancy protection if interacting with external contracts \ No newline at end of file +- Add all your requirements for purchase in `isPurchaseAllowed`, and all the additional logic in `onProductPurchase`. +- Don't add functions you don't need. For example, if you don't need to gate the purchase, don't add the `isPurchaseAllowed` function. \ No newline at end of file diff --git a/src/hooks/pricing/README.md b/src/hooks/pricing/README.md index 0f38012..23825a9 100644 --- a/src/hooks/pricing/README.md +++ b/src/hooks/pricing/README.md @@ -2,21 +2,6 @@ Calculate dynamic prices for products on Slice. Pricing strategies implement the `IProductPrice` interface to provide custom pricing logic based on various factors. -## Available Strategies - -| Strategy | Description | Use Cases | -|----------|-------------|-----------| -| **[TieredDiscount](./TieredDiscount/)** | Tiered discounts based on asset ownership | NFT holder discounts, token-based tiers | -| **[LinearVRGDAPrices](./VRGDA/LinearVRGDAPrices/)** | Linear VRGDA curve | Steady price increases over time | -| **[LogisticVRGDAPrices](./VRGDA/LogisticVRGDAPrices/)** | Logistic VRGDA curve | S-curve pricing, slow start/end | - -### VRGDA (Variable Rate Gradual Dutch Auction) - -VRGDA dynamically adjusts prices to maintain a target sales rate: -- Price increases when sales exceed target rate -- Price decreases when sales lag behind target -- Helps achieve predictable revenue over time - ## How Pricing Works The `productPrice` function is called before purchase to determine: @@ -25,111 +10,34 @@ The `productPrice` function is called before purchase to determine: ## Creating Custom Pricing -### 1. Inherit Base Contract - -```solidity -import {RegistryProductPrice, IProductsModule} from "@/utils/RegistryProductPrice.sol"; - -contract MyPricing is RegistryProductPrice { - constructor(IProductsModule productsModule) - RegistryProductPrice(productsModule) {} -} -``` - -### 2. Implement Core Functions +### Quick Start with Generator Script -```solidity -// Calculate product price -function productPrice( - uint256 slicerId, - uint256 productId, - address currency, - uint256 quantity, - address buyer, - bytes memory data -) public view override returns (uint256 ethPrice, uint256 currencyPrice) { - // Your pricing logic - // Can factor in: - // - Buyer's token/NFT holdings - // - Time since launch - // - Total sales volume - // - Custom parameters - - uint256 basePrice = getBasePrice(slicerId, productId); - uint256 discount = calculateDiscount(buyer); - - ethPrice = (basePrice * quantity * (100 - discount)) / 100; - currencyPrice = 0; // Or calculate if accepting ERC20 -} +The easiest way to create a new pricing strategy is using the hook generator: -// Configure product pricing -function _configureProduct( - uint256 slicerId, - uint256 productId, - bytes memory params -) internal override { - // Decode and store pricing parameters - (uint256 basePrice, uint256 discountRate) = abi.decode(params, (uint256, uint256)); - // Store configuration for this product -} - -// Define configuration schema -function paramsSchema() external pure override returns (string memory) { - // Schema for frontend integration - return "uint256 basePrice,uint256 discountRate"; -} -``` - -## Advanced Patterns - -### Time-Based Pricing -```solidity -uint256 elapsed = block.timestamp - launchTime; -uint256 price = basePrice * (100 + elapsed / 1 days) / 100; +```bash +# From the hooks directory +./script/generate-hook.sh ``` -### Volume Discounts -```solidity -if (quantity >= 10) price = basePrice * 90 / 100; // 10% off -if (quantity >= 50) price = basePrice * 80 / 100; // 20% off -``` +Select: +1. Registry (for Slice-integrated hooks) +2. Pricing Strategy +3. Enter your contract name +4. Enter author name (optional) -### NFT Holder Pricing -```solidity -if (IERC721(nftContract).balanceOf(buyer) > 0) { - price = basePrice * 50 / 100; // 50% discount -} -``` +The script will create your contract file with the proper template and add it to the aggregator. -## Registry Integration +### Registry Integration -Strategies inheriting from `RegistryProductPrice` automatically support: +Strategies inheriting from `RegistryProductPrice` automatically support frontend integration through: - **Product configuration** via `configureProduct()` - **Parameter validation** via `paramsSchema()` -- **Frontend discovery** through `IHookRegistry` -## Testing +### Testing -```solidity -import {RegistryProductPriceTest} from "@test/utils/RegistryProductPriceTest.sol"; - -contract MyPricingTest is RegistryProductPriceTest { - function setUp() public override { - super.setUp(); - // Your setup - } - - function test_pricing() public { - // Test different pricing scenarios - } -} -``` +The generator script will also create a test file for your pricing strategy. Customize it to your needs to test your pricing logic. ## Best Practices -- Keep calculations gas-efficient (view function) -- Consider overflow/underflow protection -- Test edge cases (quantity = 0, max uint256) -- Document pricing algorithm clearly -- Emit events if storing state (in configure) -- Consider price stability mechanisms \ No newline at end of file +- 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 diff --git a/src/hooks/pricingActions/README.md b/src/hooks/pricingActions/README.md index bfd414e..80c5c86 100644 --- a/src/hooks/pricingActions/README.md +++ b/src/hooks/pricingActions/README.md @@ -1,65 +1,43 @@ # Pricing + Actions -Combine dynamic pricing with onchain actions in a single contract. +Combine dynamic pricing with onchain actions in a single contract. These hooks implement both `IProductPrice` and `IProductAction` interfaces. -## Available Hooks +Pricing Actions are useful when: +- You want a single contract to handle both pricing and action logic +- The price logic is related to the action logic (for example, [FirstForFree](./FirstForFree/FirstForFree.sol) allows for the first item to be free for each buyer, and all future ones to be paid at a base price) -| Hook | Description | -|------|-------------| -| **[FirstForFree](./FirstForFree/)** | First purchase free based on conditions | +## How Combined Hooks Work + +Pricing actions are called at multiple points: +1. **`productPrice`** - Calculate dynamic price before purchase +2. **`isPurchaseAllowed`** - Check eligibility before payment +3. **`onProductPurchase`** - Execute custom logic after payment ## Creating Combined Hooks -### 1. Inherit Base Contract +### Quick Start with Generator Script -```solidity -import {RegistryProductPriceAction, IProductsModule} from "@/utils/RegistryProductPriceAction.sol"; +The easiest way to create a new pricing action is using the hook generator: -contract MyHook is RegistryProductPriceAction { - constructor(IProductsModule productsModule) - RegistryProductPriceAction(productsModule) {} -} +```bash +# From the hooks directory +./script/generate-hook.sh ``` -### 2. Implement All Functions - -```solidity -// Pricing logic -function productPrice(...) public view override - returns (uint256 ethPrice, uint256 currencyPrice) { - // Calculate price -} - -// Purchase eligibility -function isPurchaseAllowed(...) public view override returns (bool) { - // Check eligibility -} +Select: +1. Registry (for Slice-integrated hooks) +2. Pricing Action +3. Enter your contract name +4. Enter author name (optional) -// Purchase action -function _onProductPurchase(...) internal override { - // Execute action -} - -// Configuration -function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) - internal override { - // Store config -} - -// Parameters -function paramsSchema() external pure override returns (string memory) { - return "uint256 discount,address token"; -} -``` +The script will create your contract file with the proper template and add it to the aggregator. -## Testing +### Registry Integration -Inherit from `RegistryProductPriceActionTest` for testing: +Hooks inheriting from `RegistryProductPriceAction` automatically support frontend integration through: +- **Product configuration** via `configureProduct()` +- **Parameter validation** via `paramsSchema()` -```solidity -import {RegistryProductPriceActionTest} from "@test/utils/RegistryProductPriceActionTest.sol"; +### Testing -contract MyHookTest is RegistryProductPriceActionTest { - // Your tests -} -``` \ No newline at end of file +The generator script will also create a test file for your pricing action. Customize it to your needs to test both pricing and action logic. From a81f0fe4bf78189da646a236af12f1db17c55f26 Mon Sep 17 00:00:00 2001 From: jj_ranalli Date: Thu, 28 Aug 2025 02:56:29 +0200 Subject: [PATCH 24/24] update deps --- foundry.toml | 4 ++-- soldeer.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/foundry.toml b/foundry.toml index 25a428f..7396af8 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,7 +7,7 @@ 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"}] remappings = [ - "slice/=dependencies/slice-0.0.7/", + "slice/=dependencies/slice-0.0.8/", "@openzeppelin-4.8.0/=dependencies/@openzeppelin-contracts-4.8.0/", "@openzeppelin-upgradeable-4.8.0/=dependencies/@openzeppelin-contracts-upgradeable-4.8.0/", "@erc721a/=dependencies/erc721a-4.3.0/contracts/", @@ -44,7 +44,7 @@ remappings_generate = false remappings_regenerate = false [dependencies] -slice = "0.0.7" +slice = "0.0.8" forge-std = "1.9.7" "@openzeppelin-contracts" = "4.8.0" "@openzeppelin-contracts-upgradeable" = "4.8.0" diff --git a/soldeer.lock b/soldeer.lock index 5690771..8eca7b9 100644 --- a/soldeer.lock +++ b/soldeer.lock @@ -28,7 +28,7 @@ integrity = "9e60fdba82bc374df80db7f2951faff6467b9091873004a3d314cf0c084b3c7d" [[dependencies]] name = "slice" -version = "0.0.7" -url = "https://soldeer-revisions.s3.amazonaws.com/slice/0_0_7_23-08-2025_22:32:41_src.zip" -checksum = "ecb67f517b379576a555ea72ecfa37485c84b865dec1383e8f494479866ae236" -integrity = "0df612cdda4d727850fc5aa534986d7d5e79c599728c61cd9883792b7fd43d0a" +version = "0.0.8" +url = "https://soldeer-revisions.s3.amazonaws.com/slice/0_0_8_28-08-2025_00:55:13_src.zip" +checksum = "0df8789a04d00a6577f9be207aec4ea0057d9e79569dda522facbd7f04dbed2f" +integrity = "308b4ab7daad57e76ee001b55b2ebb19c707427bee3720fbbaad9d3c7b69021e"