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..01ab177 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,28 @@ # ▼ Slice Hooks -Smart contracts for creating custom pricing strategies and onchain actions for [Slice](https://slice.so) products. +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 +## Architecture -``` -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 -``` - -## Core Concepts - -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 -- **[`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. - -See [Hook types](#hook-types) for more details. - -## Product Purchase Lifecycle +### Product Purchase Lifecycle Here's how hooks integrate into the product purchase flow: ``` - Checkout + Checkout │ ▼ ┌─────────────────────┐ -│ Price Fetching │ ← `productPrice` called here -│ (before purchase) │ (IPricingStrategy) +│ Price Fetching │ ← `IProductPrice.productPrice()` +│ (before purchase) │ └─────────────────────┘ │ ▼ ┌─────────────────────┐ -│ Purchase Execution │ ← `onProductPurchase` called here -│ (during purchase) │ (IOnchainAction) +│ Purchase Execution │ ← `IProductAction.onProductPurchase()` +│ (during purchase) │ └─────────────────────┘ │ ▼ @@ -63,86 +37,123 @@ 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) +### Core Interfaces -Deploy once, use across multiple products with frontend integration: +Hooks are built around three main interfaces: -- **[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 +**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); +``` -### Product-Specific Hooks +**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; +``` -Tailored implementations for individual products: +**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); +``` -- **[Examples](./src/examples/)**: See real-world implementations and creation guide +### Hook Types -## Base Contracts +#### Registry Hooks (Reusable) -The base contracts in `src/utils` are designed to be inherited, providing essential building blocks for developing custom Slice hooks efficiently. +Reusable contracts designed to support multiple products, automatically integrated with Slice clients. -### Registry (Reusable): +- **[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 -- **`RegistryOnchainAction`**: Base for reusable onchain actions -- **`RegistryPricingStrategy`**: Base for reusable pricing strategies -- **`RegistryPricingStrategyAction`**: Base for reusable pricing + action hooks +#### Product-Specific Hooks -### Product-Specific +Custom smart contracts tailored for individual products, integrated using the `custom` onchain action or pricing strategy in Slice. -- **`OnchainAction`**: Base for product-specific onchain actions -- **`PricingStrategy`**: Base for product-specific pricing strategies -- **`PricingStrategyAction`**: Base for product-specific pricing + action hooks +- **[Examples](./src/examples/)**: Reference implementations and templates -## Quick Start +All hooks inherit from base contracts in `src/utils/`. -- **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/) +## Contributing -## Development +### Quick Start ```bash -forge soldeer install # Install dependencies -forge test # Run tests -forge build # Build +forge soldeer install # Install dependencies +forge build # Compile contracts +forge test # Run test suite ``` Requires [Foundry](https://book.getfoundry.sh/getting-started/installation). -### Deployment +Deploy by running `./script/deploy.sh` and following instructions -To deploy hooks, use the deployment script: +### Building a Hook + +The quickest way to create a new hook is using the interactive generator: ```bash -./script/deploy.sh +./script/generate-hook.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 +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) -When writing tests for your hooks, inherit from the appropriate base test contract: +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) -- **`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 +Once the hook is generated, add your custom contract logic to the and write tests for it. -Inheriting the appropriate test contract for your hook allows you to focus your tests solely on your custom hook logic. +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) -## Contributing +### Repository Structure -To contribute a new hook to this repository: +``` +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 +``` -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 +### Resources -Make sure your contribution follows the existing code style and includes proper documentation. \ 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/deployments/addresses.json b/deployments/addresses.json index fe65f1e..c3a44b1 100644 --- a/deployments/addresses.json +++ b/deployments/addresses.json @@ -1,5 +1,80 @@ { - "actions": {}, - "pricingStrategies": {}, - "pricingStrategyActions": {} + "actions": { + "Allowlisted": [ + { + "address": "0x157428DD791E03c20880D22C3dA2B66A36B5cF26", + "blockNumber": 33510607, + "paramsSchema": "bytes32 merkleRoot", + "transactionHash": "0x6af9ac700f1c9a38de57fa4bc13262162d9b674649fd417ccd50237b6cfbc178" + } + ], + "ERC20Gated": [ + { + "address": "0x26A1C86B555013995Fc72864D261fDe984752E7c", + "blockNumber": 33558792, + "paramsSchema": "(address erc20,uint256 amount)[] erc20Gates", + "transactionHash": "0x3a7c01ede05a34280073479d5cdf1f35e41d9f08c36a71f358b9c503ccc54526" + } + ], + "ERC20Mint": [ + { + "address": "0x67f9799FaC1D53C63217BEE47f553150F5BB0836", + "blockNumber": 33520592, + "paramsSchema": "string name,string symbol,uint256 premintAmount,address premintReceiver,bool revertOnMaxSupplyReached,uint256 maxSupply,uint256 tokensPerUnit", + "transactionHash": "0xfcd7e8fe47aa509afa0acdef86b741656c2602626f3258f8a83b359c665d6b04" + } + ], + "ERC721Mint": [ + { + "address": "0x2b6488115FAa50142E140172CbCd60e6370675F7", + "blockNumber": 33511082, + "paramsSchema": "string name,string symbol,address royaltyReceiver,uint256 royaltyFraction,string baseURI,string tokenURI,bool revertOnMaxSupplyReached,uint256 maxSupply", + "transactionHash": "0x4981b3b67c0b8abe6a942b3ca86643f2b1dfdbcce59d6c819ad20a82814956e0" + } + ], + "NFTGated": [ + { + "address": "0xD4eF7A46bF4c58036eaCA886119F5230e5a2C25d", + "blockNumber": 33563508, + "paramsSchema": "(address nft,uint8 nftType,uint80 id,uint8 minQuantity)[] nftGates,uint256 minOwned", + "transactionHash": "0x9dd101a6155b432849cb33f65c5d4b9f6875e6b1e7bf806005a8a6b0d070f634" + } + ] + }, + "pricing": { + "NFTDiscount": [ + { + "address": "0xb830a457d2f51d4cA1136b97FB30DF6366CFe2f5", + "blockNumber": 33596708, + "paramsSchema": "(address nft,uint80 discount,uint8 minQuantity,uint8 nftType,uint256 tokenId)[] discounts", + "transactionHash": "0x966be5fa1da3fab7c5027c9acb3f118307997e30687188b4745d74fd26ad9e7e" + } + ], + "LinearVRGDAPrices": [ + { + "address": "0xEC68E30182F4298b7032400B7ce809da613e4449", + "blockNumber": 33511188, + "paramsSchema": "(address currency,int128 targetPrice,uint128 min,int256 perTimeUnit)[] linearParams,int256 priceDecayPercent", + "transactionHash": "0xf2667ce20d07561e59c8d3e3de135bbd895c5b08bb57fe487c6c8197c91a0d73" + } + ], + "LogisticVRGDAPrices": [ + { + "address": "0x2b02cC8528EF18abf8185543CEC29A94F0542c8F", + "blockNumber": 33511209, + "paramsSchema": "(address currency,int128 targetPrice,uint128 min,int256 timeScale)[] logisticParams,int256 priceDecayPercent", + "transactionHash": "0x2b27380661a38b54dbccf634305622296034ddfccf5865a07fbfb7810ea41025" + } + ] + }, + "pricingActions": { + "FirstForFree": [ + { + "address": "0x2C18D37b8229233F672bF406bCe8799BCfD43B5A", + "blockNumber": 33510960, + "paramsSchema": "uint256 usdcPrice,(address tokenAddress,uint8 tokenType,uint88 tokenId,uint8 minQuantity)[] eligibleTokens,address mintToken,uint88 mintTokenId,uint8 freeUnits", + "transactionHash": "0x8bd0b9de8edd704899210f8450096adf7f4f853030a7ba0a2f64494bc1ba726e" + } + ] + } } diff --git a/foundry.toml b/foundry.toml index 0eeb8bc..7396af8 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,10 +7,10 @@ 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.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/", - "@murky/=dependencies/murky-0.1.0/src/", "forge-std/=dependencies/forge-std-1.9.7/src/", "@test/=test/", "@/=src/" @@ -44,9 +44,12 @@ remappings_generate = false remappings_regenerate = false [dependencies] -slice = "0.0.4" +slice = "0.0.8" 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"] diff --git a/script/ScriptUtils.sol b/script/ScriptUtils.sol index d604d0c..5d612f6 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; } @@ -134,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); } @@ -210,6 +213,7 @@ abstract contract SetUpContractsList is Script { json = new string[](existingContractAddresses.length + 1); vm.serializeAddress("0", "address", transaction.contractAddress); vm.serializeUint("0", "blockNumber", receipt.blockNumber); + vm.serializeString("0", "paramsSchema", IHookRegistry(transaction.contractAddress).paramsSchema()); json[0] = vm.serializeBytes32("0", "transactionHash", transaction.hash); for (uint256 i = 0; i < existingContractAddresses.length; i++) { @@ -218,12 +222,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 +417,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 +425,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 +443,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 +454,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 +488,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/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/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 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/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/soldeer.lock b/soldeer.lock index 3f903d8..8eca7b9 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" @@ -19,16 +26,9 @@ 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.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.8" +url = "https://soldeer-revisions.s3.amazonaws.com/slice/0_0_8_28-08-2025_00:55:13_src.zip" +checksum = "0df8789a04d00a6577f9be207aec4ea0057d9e79569dda522facbd7f04dbed2f" +integrity = "308b4ab7daad57e76ee001b55b2ebb19c707427bee3720fbbaad9d3c7b69021e" diff --git a/src/examples/README.md b/src/examples/README.md index 6861892..5a75896 100644 --- a/src/examples/README.md +++ b/src/examples/README.md @@ -1,135 +1,120 @@ -# 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. +Reference implementations for custom product hooks without registry support. -## Key Interfaces +## When to Use -**IPricingStrategy**: -```solidity -interface IPricingStrategy { - function productPrice( - uint256 slicerId, - uint256 productId, - address currency, - uint256 quantity, - address buyer, - bytes memory data - ) external view returns (uint256 ethPrice, uint256 currencyPrice); -} +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 + +| Registry Hooks | Product-Specific | +|----------------|------------------| +| ✓ Reusable across products | ✗ Single product only | +| ✓ Frontend auto-integration | ✗ Manual integration | +| ✓ Parameter validation | ✗ Hardcoded config | +| ✗ More complex | ✓ Simpler setup | + +## 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 ``` -**IOnchainAction**: +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 -interface IOnchainAction { +import {ProductAction, IProductsModule, IProductAction} from "@/utils/ProductAction.sol"; + +contract MyProductAction is ProductAction { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(IProductsModule productsModuleAddress, uint256 slicerId) + ProductAction(productsModuleAddress, slicerId) + {} + + /*////////////////////////////////////////////////////////////// + CONFIGURATION + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IProductAction + */ function isPurchaseAllowed( uint256 slicerId, uint256 productId, - address account, + address buyer, uint256 quantity, bytes memory slicerCustomData, bytes memory buyerCustomData - ) external view returns (bool); - - function onProductPurchase( + ) 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 account, + address buyer, uint256 quantity, bytes memory slicerCustomData, bytes memory buyerCustomData - ) external payable; + ) internal override { + // Your logic to be executed after product purchase. + } } ``` -## 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 - -## 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`) -- Don't implement `IHookRegistry` (not intended for Slice frontend integration) -- Serve as reference implementations and starting points - -## Available Examples +### Pricing Example -### Actions - -- **[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. - -## Creating Custom Product-Specific Hooks - -### Onchain Action - -To create a custom product-specific onchain action: - -1. **Inherit from OnchainAction**: ```solidity -import {OnchainAction, IProductsModule} from "@/utils/OnchainAction.sol"; +import {ProductPrice, IProductsModule, IProductPrice} from "@/utils/ProductPrice.sol"; -contract MyProductAction is OnchainAction { - constructor(IProductsModule productsModule, uint256 slicerId) - OnchainAction(productsModule, slicerId) {} -} -``` +contract MyProductPrice is ProductPrice { + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ -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 -} -``` + constructor(IProductsModule productsModuleAddress, uint256 slicerId) + ProductPrice(productsModuleAddress) + {} -### Pricing Strategy - -To create a custom product-specific pricing strategy: - -1. **Inherit from PricingStrategy**: -```solidity -import {PricingStrategy, IProductsModule} from "@/utils/PricingStrategy.sol"; + /*////////////////////////////////////////////////////////////// + CONFIGURATION + //////////////////////////////////////////////////////////////*/ -contract MyProductAction is PricingStrategy { - constructor(IProductsModule productsModule, uint256 slicerId) - PricingStrategy(productsModule, slicerId) {} -} -``` - -2. **Implement required functions**: -```solidity -function productPrice(...) public view override returns (uint256 ethPrice, uint256 currencyPrice) { - // Your pricing logic here + /** + * @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. + } } ``` - -## Using These Examples - -These examples show common patterns for: -- Product-specific NFT minting -- Integration with external contracts -- Onchain rewards - -Copy and modify these examples to create your own product-specific implementations. \ No newline at end of file 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 cadb88d..01e7811 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,20 +26,20 @@ 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( uint256 slicerId, uint256 productId, - address account, + address buyer, uint256, bytes memory, bytes memory buyerCustomData @@ -47,8 +47,16 @@ contract Allowlisted is RegistryOnchainAction { // Get Merkle proof from buyerCustomData bytes32[] memory proof = abi.decode(buyerCustomData, (bytes32[])); + uint256 leafValue = uint256(uint160(buyer)); + // 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/ERC20Gated/ERC20Gated.sol b/src/hooks/actions/ERC20Gated/ERC20Gated.sol index bbc4d4e..2072bce 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,29 +26,27 @@ 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( - 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; } @@ -64,6 +62,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..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. */ @@ -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/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/ERC721AMint/ERC721Mint.sol b/src/hooks/actions/ERC721Mint/ERC721Mint.sol similarity index 91% rename from src/hooks/actions/ERC721AMint/ERC721Mint.sol rename to src/hooks/actions/ERC721Mint/ERC721Mint.sol index a606d5b..6e70849 100644 --- a/src/hooks/actions/ERC721AMint/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/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 96% rename from src/hooks/actions/ERC721AMint/utils/ERC721Mint_BaseToken.sol rename to src/hooks/actions/ERC721Mint/utils/ERC721Mint_BaseToken.sol index b43f474..3107127 100644 --- a/src/hooks/actions/ERC721AMint/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/actions/NFTGated/NFTGated.sol b/src/hooks/actions/NFTGated/NFTGated.sol index 15d195b..6301723 100644 --- a/src/hooks/actions/NFTGated/NFTGated.sol +++ b/src/hooks/actions/NFTGated/NFTGated.sol @@ -5,19 +5,19 @@ 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"; -import {TokenType, NFTGate, NFTGates} from "./types/NFTGate.sol"; +} from "@/utils/RegistryProductAction.sol"; +import {NftType, NFTGate, NFTGates} from "./types/NFTGate.sol"; /** * @title NFTGated * @notice Onchain action registry for NFT gating. * @author Slice */ -contract NFTGated is RegistryOnchainAction { +contract NFTGated is RegistryProductAction { /*////////////////////////////////////////////////////////////// MUTABLE STORAGE //////////////////////////////////////////////////////////////*/ @@ -28,24 +28,22 @@ 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( - 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; @@ -53,11 +51,11 @@ contract NFTGated is RegistryOnchainAction { for (uint256 i; i < nftGates_.gates.length;) { NFTGate memory gate = nftGates_.gates[i]; - if (gate.tokenType == TokenType.ERC1155) { - if (IERC1155(gate.nft).balanceOf(account, gate.id) >= gate.minQuantity) { + if (gate.nftType == NftType.ERC1155) { + 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; } @@ -75,6 +73,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]); @@ -85,6 +85,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/actions/README.md b/src/hooks/actions/README.md index 9adb197..4770360 100644 --- a/src/hooks/actions/README.md +++ b/src/hooks/actions/README.md @@ -1,83 +1,44 @@ # 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. - -## Key Interface: IOnchainAction - -```solidity -interface IOnchainAction { - 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: RegistryOnchainAction +Execute custom logic when products are purchased on Slice. Actions implement the `IProductAction` interface to control purchase eligibility and perform operations during transactions. -All actions in this directory inherit from `RegistryOnchainAction`, which provides: -- Registry functionality for reusable hooks across multiple products -- Implementation of `IHookRegistry` for Slice frontend integration -- Base implementations for common patterns +## How Actions Work -## Available Actions +Actions are called at two points during purchase: -- **[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. +1. **`isPurchaseAllowed`** - Before payment, checks if buyer can purchase +2. **`onProductPurchase`** - After payment, executes custom logic ## Creating Custom Actions -To create a custom onchain action: +### Quick Start with Generator Script -1. **Inherit from RegistryOnchainAction**: -```solidity -import {RegistryOnchainAction, IProductsModule} from "@/utils/RegistryOnchainAction.sol"; +The easiest way to create a new action is using the hook generator: -contract MyAction is RegistryOnchainAction { - constructor(IProductsModule productsModule) - RegistryOnchainAction(productsModule) {} -} +```bash +# From the hooks directory +./script/generate-hook.sh ``` -2. **Implement required functions**: -```solidity -function isPurchaseAllowed(...) public view override returns (bool) { - // Your eligibility logic here -} +Select: +1. Registry (for Slice-integrated hooks) +2. Onchain Action +3. Enter your contract name +4. Enter author name (optional) -function _onProductPurchase(...) internal override { - // Custom logic to execute on purchase -} +The script will create your contract file with the proper template and add it to the aggregator. -function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) - internal override { - // Handle product configuration -} +### Registry Integration -function paramsSchema() external pure override returns (string memory) { - return "uint256 param1,address param2"; // Your parameter schema -} -``` +Actions inheriting from `RegistryProductAction` automatically support frontend integration through: +- **Product configuration** via `configureProduct()` +- **Parameter validation** via `paramsSchema()` + +### Testing + +The generator script will also create a test file for your action. Customize it to your needs to test your action. -## Integration with Slice +## Best Practices -Actions that inherit from `RegistryOnchainAction` 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 +- 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/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/README.md b/src/hooks/pricing/README.md index 2534bba..23825a9 100644 --- a/src/hooks/pricing/README.md +++ b/src/hooks/pricing/README.md @@ -1,69 +1,43 @@ # 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. +Calculate dynamic prices for products on Slice. Pricing strategies implement the `IProductPrice` interface to provide custom pricing logic based on various factors. -## Key Interface: IPricingStrategy +## How Pricing Works -```solidity -interface IPricingStrategy { - function productPrice( - uint256 slicerId, - uint256 productId, - address currency, - uint256 quantity, - address buyer, - bytes memory data - ) external view returns (uint256 ethPrice, uint256 currencyPrice); -} -``` +The `productPrice` function is called before purchase to determine: +- **ETH price** - Price in native currency +- **Currency price** - Price in ERC20 tokens (if applicable) -## Base Contract: RegistryPricingStrategy +## Creating Custom Pricing -All pricing strategies in this directory inherit from `RegistryPricingStrategy`, which provides: -- Registry functionality for reusable pricing across multiple products -- Implementation of `IHookRegistry` for Slice frontend integration -- Base implementations for common patterns +### Quick Start with Generator Script -## Available Strategies +The easiest way to create a new pricing strategy is using the hook generator: -- **[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 +```bash +# From the hooks directory +./script/generate-hook.sh +``` -## Creating Custom Pricing Strategies +Select: +1. Registry (for Slice-integrated hooks) +2. Pricing Strategy +3. Enter your contract name +4. Enter author name (optional) -To create a custom pricing strategy: +The script will create your contract file with the proper template and add it to the aggregator. -1. **Inherit from RegistryPricingStrategy**: -```solidity -import {RegistryPricingStrategy, IProductsModule} from "@/utils/RegistryPricingStrategy.sol"; +### Registry Integration -contract MyPricingStrategy is RegistryPricingStrategy { - constructor(IProductsModule productsModule) - RegistryPricingStrategy(productsModule) {} -} -``` +Strategies inheriting from `RegistryProductPrice` automatically support frontend integration through: +- **Product configuration** via `configureProduct()` +- **Parameter validation** via `paramsSchema()` -2. **Implement required functions**: -```solidity -function productPrice(...) public view override returns (uint256 ethPrice, uint256 currencyPrice) { - // Your pricing logic here -} +### Testing -function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) - internal override { - // Handle product configuration -} - -function paramsSchema() external pure override returns (string memory) { - return "uint256 basePrice,uint256 multiplier"; // Your parameter schema -} -``` +The generator script will also create a test file for your pricing strategy. Customize it to your needs to test your pricing logic. -## Integration with Slice +## Best Practices -Pricing strategies that inherit from `RegistryPricingStrategy` 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 +- 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/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol index 5ac9380..a118958 100644 --- a/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol @@ -3,9 +3,9 @@ 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 {HookRegistry, IHookRegistry, IProductsModule} from "@/utils/RegistryProductPrice.sol"; +import {DiscountParams, TieredDiscount} from "../TieredDiscount.sol"; +import {NFTType} from "../types/DiscountParams.sol"; /** * @title NFTDiscount @@ -26,74 +26,38 @@ 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]; + + delete 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; - - // Set values used in inner loop - newDiscounts = currencyParams.discounts; - prevDiscountsLength = productDiscount.discountsArray.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 - if (currencyParams.discountType == DiscountType.Relative) { - if (newDiscounts[j].discount > 1e4) { - revert InvalidRelativeDiscount(); - } - } - - 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.discountsArray[j] = newDiscounts[j]; - } else if (j >= prevDiscountsLength) { - // Append new discounts - productDiscount.discountsArray.push(newDiscounts[j]); - } - } else { - // Remove old discounts - productDiscount.discountsArray.pop(); - } + DiscountParams memory discountParam; + for (uint256 i; i < newDiscounts.length;) { + discountParam = newDiscounts[i]; + + // Check relative discount doesn't exceed max value of 1e4 (100%) + if (discountParam.discount > 1e4) { + revert InvalidRelativeAmount(); + } - 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; @@ -105,8 +69,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 +83,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 +109,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 +156,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..ce1d270 100644 --- a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol @@ -1,22 +1,22 @@ // 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 {RegistryProductPrice, IProductPrice} from "@/utils/RegistryProductPrice.sol"; +import {DiscountParams} from "./types/DiscountParams.sol"; /** * @title TieredDiscount * @notice Tiered discounts based on asset ownership * @author Slice */ -abstract contract TieredDiscount is RegistryPricingStrategy { +abstract contract TieredDiscount is RegistryProductPrice { /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ error WrongCurrency(); - error InvalidRelativeDiscount(); + error InvalidRelativeAmount(); error InvalidMinQuantity(); error DiscountsNotDescending(DiscountParams nft); @@ -24,21 +24,20 @@ 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 //////////////////////////////////////////////////////////////*/ - constructor(IProductsModule productsModuleAddress) RegistryPricingStrategy(productsModuleAddress) {} + constructor(IProductsModule productsModuleAddress) RegistryProductPrice(productsModuleAddress) {} /*////////////////////////////////////////////////////////////// FUNCTIONS //////////////////////////////////////////////////////////////*/ /** - * @inheritdoc IPricingStrategy + * @inheritdoc IProductPrice */ function productPrice( uint256 slicerId, @@ -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/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/pricing/pricing.sol b/src/hooks/pricing/pricing.sol new file mode 100644 index 0000000..47b0454 --- /dev/null +++ b/src/hooks/pricing/pricing.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {LinearVRGDAPrices} from "./VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol"; +import {LogisticVRGDAPrices} from "./VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol"; +import {NFTDiscount} from "./TieredDiscount/NFTDiscount/NFTDiscount.sol"; diff --git a/src/hooks/pricingActions/FirstForFree/FirstForFree.sol b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol index 8de82ed..f9ec506 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, + IProductPrice, + RegistryProductAction, + RegistryProductPriceAction, IHookRegistry, IProductsModule -} from "@/utils/RegistryPricingStrategyAction.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"; @@ -21,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 //////////////////////////////////////////////////////////////*/ @@ -33,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) @@ -69,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..80c5c86 100644 --- a/src/hooks/pricingActions/README.md +++ b/src/hooks/pricingActions/README.md @@ -1,98 +1,43 @@ -# Pricing Strategy Actions +# Pricing + 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. +Combine dynamic pricing with onchain actions in a single contract. These hooks implement both `IProductPrice` and `IProductAction` interfaces. -## Key Interfaces +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) -**IPricingStrategy**: -```solidity -interface IPricingStrategy { - function productPrice( - uint256 slicerId, - uint256 productId, - address currency, - uint256 quantity, - address buyer, - bytes memory data - ) external view returns (uint256 ethPrice, uint256 currencyPrice); -} -``` - -**IOnchainAction**: -```solidity -interface IOnchainAction { - 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: RegistryPricingStrategyAction +## How Combined Hooks Work -All pricing strategy actions inherit from `RegistryPricingStrategyAction`, 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 +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 -## Available Pricing Strategy Actions +## Creating Combined Hooks -- **[FirstForFree](./FirstForFree/FirstForFree.sol)**: Discounts the first purchase of a product for free, based on conditions. +### Quick Start with Generator Script -## Creating Custom Pricing Strategy Actions +The easiest way to create a new pricing action is using the hook generator: -To create a custom pricing strategy action: - -1. **Inherit from RegistryPricingStrategyAction**: -```solidity -import {RegistryPricingStrategyAction, IProductsModule} from "@/utils/RegistryPricingStrategyAction.sol"; - -contract MyPricingStrategyAction is RegistryPricingStrategyAction { - constructor(IProductsModule productsModule) - RegistryPricingStrategyAction(productsModule) {} -} +```bash +# From the hooks directory +./script/generate-hook.sh ``` -2. **Implement required functions**: -```solidity -function productPrice(...) public view override returns (uint256 ethPrice, uint256 currencyPrice) { - // Your pricing logic here -} +Select: +1. Registry (for Slice-integrated hooks) +2. Pricing Action +3. Enter your contract name +4. Enter author name (optional) -function isPurchaseAllowed(...) public view override returns (bool) { - // Your eligibility logic here -} +The script will create your contract file with the proper template and add it to the aggregator. -function _onProductPurchase(...) internal override { - // Custom logic to execute on purchase -} +### Registry Integration -function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) - internal override { - // Handle product configuration -} - -function paramsSchema() external pure override returns (string memory) { - return "uint256 param1,address param2"; // Your parameter schema -} -``` +Hooks inheriting from `RegistryProductPriceAction` automatically support frontend integration through: +- **Product configuration** via `configureProduct()` +- **Parameter validation** via `paramsSchema()` -## Integration with Slice +### Testing -Pricing strategy actions that inherit from `RegistryPricingStrategyAction` 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 +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. 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"; 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 2678432..c8ea84d 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 {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; -contract AllowlistedTest is RegistryOnchainActionTest { +contract AllowlistedTest is RegistryProductActionTest { Allowlisted allowlisted; Merkle m; bytes32[] data; @@ -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))); + } } diff --git a/test/actions/ERC20Gated/ERC20Gated.t.sol b/test/actions/ERC20Gated/ERC20Gated.t.sol index 6746e4e..1806ab7 100644 --- a/test/actions/ERC20Gated/ERC20Gated.t.sol +++ b/test/actions/ERC20Gated/ERC20Gated.t.sol @@ -1,19 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {RegistryOnchainActionTest} from "@test/utils/RegistryOnchainActionTest.sol"; -import {ERC20Gated, ERC20Gate} from "@/hooks/actions/ERC20Gated/ERC20Gated.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"; uint256 constant slicerId = 0; uint256 constant productId = 1; -contract ERC20GatedTest is RegistryOnchainActionTest { - ERC20Gated erc20Gated; - MockERC20 token = new MockERC20(); +contract ERC20GatedTest is RegistryProductActionTest { + MockERC20Gated erc20Gated; + MockERC20 token = new MockERC20("Test", "TST", 18); + MockERC20 token2 = new MockERC20("Test2", "TST2", 18); 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("Test3", "TST3", 18); + gates = new ERC20Gate[](1); + gates[0] = ERC20Gate(token3, 300); + + erc20Gated.configureProduct(slicerId, productId, abi.encode(gates)); + assertEq(address(erc20Gated.gates(slicerId, productId)[0].erc20), address(token3)); + assertEq(erc20Gated.gates(slicerId, productId)[0].amount, 300); + assertEq(erc20Gated.gates(slicerId, productId).length, 1); + + 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/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/ERC721AMint/ERC721Mint.t.sol b/test/actions/ERC721Mint/ERC721Mint.t.sol similarity index 97% rename from test/actions/ERC721AMint/ERC721Mint.t.sol rename to test/actions/ERC721Mint/ERC721Mint.t.sol index f76a30c..cee15be 100644 --- a/test/actions/ERC721AMint/ERC721Mint.t.sol +++ b/test/actions/ERC721Mint/ERC721Mint.t.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: MIT 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 {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"; 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 2a769af..6e3134f 100644 --- a/test/actions/NFTGated/NFTGated.t.sol +++ b/test/actions/NFTGated/NFTGated.t.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {RegistryOnchainActionTest} from "@test/utils/RegistryOnchainActionTest.sol"; -import {NFTGated, NFTGates, NFTGate, TokenType} from "@/hooks/actions/NFTGated/NFTGated.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"; import {MockERC1155} from "@test/utils/mocks/MockERC1155.sol"; @@ -10,15 +11,15 @@ import {console2} from "forge-std/console2.sol"; uint256 constant slicerId = 0; -contract NFTGatedTest is RegistryOnchainActionTest { - NFTGated nftGated; +contract NFTGatedTest is RegistryProductActionTest { + 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(); @@ -74,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/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; + } +} diff --git a/test/pricing/TieredDiscount/NFTDiscount.t.sol b/test/pricing/TieredDiscount/NFTDiscount.t.sol new file mode 100644 index 0000000..77d0a7e --- /dev/null +++ b/test/pricing/TieredDiscount/NFTDiscount.t.sol @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {RegistryProductPriceTest} from "@test/utils/RegistryProductPriceTest.sol"; +import {console2} from "forge-std/console2.sol"; +import { + IProductsModule, + NFTDiscount, + DiscountParams, + NFTType +} from "@/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol"; +import {MockERC721} from "@test/utils/mocks/MockERC721.sol"; +import {MockERC1155} from "@test/utils/mocks/MockERC1155.sol"; + +address constant USDC = address(1); +uint256 constant slicerId = 0; +uint256 constant productId = 1; +uint80 constant percentDiscountOne = 1000; // 10% +uint80 constant percentDiscountTwo = 2000; // 20% + +contract NFTDiscountTest is RegistryProductPriceTest { + NFTDiscount erc721GatedDiscount; + MockERC721 nftOne = new MockERC721(); + MockERC721 nftTwo = new MockERC721(); + MockERC721 nftThree = new MockERC721(); + MockERC1155 nft1155 = new MockERC1155(); + + uint256 quantity = 1; + uint8 minNftQuantity = 1; + + function setUp() public { + erc721GatedDiscount = new NFTDiscount(PRODUCTS_MODULE); + _setHook(address(erc721GatedDiscount)); + + nftOne.mint(buyer); + } + + function testConfigureProduct__ETH() public { + DiscountParams[] memory discountParams = new DiscountParams[](1); + + /// set product price with additional custom inputs + discountParams[0] = DiscountParams({ + nft: address(nftOne), + discount: percentDiscountOne, + minQuantity: minNftQuantity, + nftType: NFTType.ERC721, + tokenId: 0 + }); + + vm.prank(productOwner); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + + /// check product price + (uint256 ethPrice, uint256 currencyPrice) = + erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + + (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 discountParams = new DiscountParams[](1); + + /// set product price with additional custom inputs + discountParams[0] = DiscountParams({ + nft: address(nftOne), + discount: percentDiscountOne, + minQuantity: minNftQuantity, + nftType: NFTType.ERC721, + tokenId: 0 + }); + + vm.prank(productOwner); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + + /// check product price + (uint256 ethPrice, uint256 currencyPrice) = + erc721GatedDiscount.productPrice(slicerId, productId, USDC, quantity, buyer, ""); + + (, 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 discountParams = new DiscountParams[](1); + + /// set product price with additional custom inputs + discountParams[0] = DiscountParams({ + nft: address(nft1155), + discount: percentDiscountOne, + minQuantity: minNftQuantity, + nftType: NFTType.ERC1155, + tokenId: 1 + }); + + vm.prank(productOwner); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + + /// check product price + (uint256 ethPrice, uint256 currencyPrice) = + erc721GatedDiscount.productPrice(slicerId, productId, USDC, quantity, buyer, ""); + + (, 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, ""); + + assertEq(currencyPrice, quantity * (baseCurrencyPrice - (baseCurrencyPrice * percentDiscountOne) / 1e4)); + assertEq(ethPrice, 0); + } + + function testConfigureProduct__HigherDiscount() public { + DiscountParams[] memory discountParams = new DiscountParams[](2); + + discountParams[0] = DiscountParams({ + nft: address(nft1155), + discount: percentDiscountTwo, + minQuantity: minNftQuantity, + nftType: NFTType.ERC1155, + tokenId: 1 + }); + discountParams[1] = DiscountParams({ + nft: address(nftOne), + discount: percentDiscountOne, + minQuantity: minNftQuantity, + nftType: NFTType.ERC721, + tokenId: 0 + }); + + vm.prank(productOwner); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + + /// check product price for ETH + (uint256 ethPrice, uint256 currencyPrice) = + erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + + (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + + assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); + assertEq(currencyPrice, 0); + + nft1155.mint(buyer); + + (ethPrice, currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + + assertEq(ethPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountTwo) / 1e4)); + assertEq(currencyPrice, 0); + } + + function testRevert_ProductPrice__NotNFTOwner() public { + DiscountParams[] memory discountParams = new DiscountParams[](1); + + /// set product price for NFT that is not owned by buyer + discountParams[0] = DiscountParams({ + nft: address(nftTwo), + discount: percentDiscountOne, + minQuantity: minNftQuantity, + nftType: NFTType.ERC721, + tokenId: 0 + }); + + vm.prank(productOwner); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + + /// check product price + (uint256 ethPrice, uint256 currencyPrice) = + erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + + (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + + assertEq(ethPrice, quantity * baseEthPrice); + assertEq(currencyPrice, 0); + } + + function testProductPrice__MinQuantity() public { + DiscountParams[] memory discountParams = new DiscountParams[](1); + + /// Buyer owns 1 NFT, but minQuantity is 2 + discountParams[0] = DiscountParams({ + nft: address(nftOne), + discount: percentDiscountOne, + minQuantity: minNftQuantity + 1, + nftType: NFTType.ERC721, + tokenId: 0 + }); + + vm.prank(productOwner); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + + /// check product price + (uint256 ethPrice, uint256 currencyPrice) = + erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + + (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); + + /// check product price + (uint256 secondEthPrice, uint256 secondCurrencyPrice) = + erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + + assertEq(secondEthPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); + assertEq(secondCurrencyPrice, 0); + } + + function testProductPrice__MultipleBoughtQuantity() public { + DiscountParams[] memory discountParams = new DiscountParams[](1); + + discountParams[0] = DiscountParams({ + nft: address(nftOne), + discount: percentDiscountOne, + minQuantity: minNftQuantity, + nftType: NFTType.ERC721, + tokenId: 0 + }); + + vm.prank(productOwner); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + + // buy multiple products + quantity = 6; + + /// check product price + (uint256 ethPrice, uint256 currencyPrice) = + erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + + (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 discountParams = new DiscountParams[](1); + + discountParams[0] = DiscountParams({ + nft: address(nftTwo), + discount: percentDiscountTwo, + minQuantity: minNftQuantity, + nftType: NFTType.ERC721, + tokenId: 0 + }); + + vm.prank(productOwner); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + + // mint NFT 2 + nftTwo.mint(buyer); + + /// check product price + (uint256 ethPrice, uint256 currencyPrice) = + erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + + (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + + 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 + discountParams[0] = DiscountParams({ + nft: address(nftThree), + discount: percentDiscountOne + 10, + minQuantity: minNftQuantity, + nftType: NFTType.ERC721, + tokenId: 0 + }); + + discountParams[1] = DiscountParams({ + nft: address(nftOne), + discount: percentDiscountOne, + minQuantity: minNftQuantity, + nftType: NFTType.ERC721, + tokenId: 0 + }); + + vm.prank(productOwner); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + + /// check product price + (uint256 secondEthPrice, uint256 secondCurrencyPrice) = + erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + + assertEq(secondEthPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); + assertEq(secondCurrencyPrice, 0); + } + + function testConfigureProduct__Edit_Remove() public { + 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 + discountParams[0] = DiscountParams({ + nft: address(nftThree), + discount: percentDiscountOne + 10, + minQuantity: minNftQuantity, + nftType: NFTType.ERC721, + tokenId: 0 + }); + + discountParams[1] = DiscountParams({ + nft: address(nftOne), + discount: percentDiscountOne, + minQuantity: minNftQuantity, + nftType: NFTType.ERC721, + tokenId: 0 + }); + + vm.prank(productOwner); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + + /// check product price + (uint256 secondEthPrice, uint256 secondCurrencyPrice) = + erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + + (uint256 baseEthPrice,) = PRODUCTS_MODULE.basePrice(slicerId, productId, ETH, quantity); + + assertEq(secondEthPrice, quantity * (baseEthPrice - (baseEthPrice * percentDiscountOne) / 1e4)); + assertEq(secondCurrencyPrice, 0); + + discountParams = new DiscountParams[](0); + + vm.prank(productOwner); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(discountParams)); + + /// check product price + (uint256 ethPrice, uint256 currencyPrice) = + erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); + + assertEq(ethPrice, quantity * baseEthPrice); + assertEq(currencyPrice, 0); + } +} diff --git a/test/pricingStrategies/VRGDA/LinearVRGDA.t.sol b/test/pricing/VRGDA/LinearVRGDA.t.sol similarity index 97% rename from test/pricingStrategies/VRGDA/LinearVRGDA.t.sol rename to test/pricing/VRGDA/LinearVRGDA.t.sol index fb2a617..249bb26 100644 --- a/test/pricingStrategies/VRGDA/LinearVRGDA.t.sol +++ b/test/pricing/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/pricing/VRGDA/LogisticVRGDA.t.sol similarity index 97% rename from test/pricingStrategies/VRGDA/LogisticVRGDA.t.sol rename to test/pricing/VRGDA/LogisticVRGDA.t.sol index 8846949..ce4bb18 100644 --- a/test/pricingStrategies/VRGDA/LogisticVRGDA.t.sol +++ b/test/pricing/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 { @@ -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); diff --git a/test/pricingStrategies/VRGDA/correctness/LinearVRGDACorrectness.t.sol b/test/pricing/VRGDA/correctness/LinearVRGDACorrectness.t.sol similarity index 95% rename from test/pricingStrategies/VRGDA/correctness/LinearVRGDACorrectness.t.sol rename to test/pricing/VRGDA/correctness/LinearVRGDACorrectness.t.sol index 2dee0bd..5cb158c 100644 --- a/test/pricingStrategies/VRGDA/correctness/LinearVRGDACorrectness.t.sol +++ b/test/pricing/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/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/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.sol b/test/pricingStrategies/TieredDiscount/NFTDiscount.sol deleted file mode 100644 index be445e8..0000000 --- a/test/pricingStrategies/TieredDiscount/NFTDiscount.sol +++ /dev/null @@ -1,439 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {RegistryPricingStrategyTest} from "@test/utils/RegistryPricingStrategyTest.sol"; -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"; -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; -uint80 constant fixedDiscountOne = 100; -uint80 constant fixedDiscountTwo = 200; -uint80 constant percentDiscount = 1000; // 10% - -contract NFTDiscountTest is RegistryPricingStrategyTest { - NFTDiscount erc721GatedDiscount; - MockERC721 nftOne = new MockERC721(); - MockERC721 nftTwo = new MockERC721(); - MockERC721 nftThree = new MockERC721(); - MockERC1155 nft1155 = new MockERC1155(); - - uint240 basePrice = 1000; - uint256 quantity = 1; - uint8 minNftQuantity = 1; - - function setUp() public { - erc721GatedDiscount = new NFTDiscount(PRODUCTS_MODULE); - _setHook(address(erc721GatedDiscount)); - - 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); - - /// set product price with additional custom inputs - discounts[0] = DiscountParams({ - nft: address(nftOne), - discount: fixedDiscountOne, - 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)); - - /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - - assertTrue(ethPrice == quantity * (basePrice - fixedDiscountOne)); - assertTrue(currencyPrice == 0); - } - - function testConfigureProduct__ERC20() public { - DiscountParams[] memory discounts = new DiscountParams[](1); - - /// set product price with additional custom inputs - discounts[0] = DiscountParams({ - nft: address(nftOne), - discount: fixedDiscountOne, - 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)); - - /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, USDC, quantity, buyer, ""); - - assertTrue(currencyPrice == quantity * (basePrice - fixedDiscountOne)); - assertTrue(ethPrice == 0); - } - - function testConfigureProduct__ERC1155() public { - DiscountParams[] memory discounts = new DiscountParams[](1); - - /// set product price with additional custom inputs - discounts[0] = DiscountParams({ - nft: address(nft1155), - discount: fixedDiscountOne, - 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)); - - /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, USDC, quantity, buyer, ""); - - assertTrue(currencyPrice == quantity * basePrice); - assertTrue(ethPrice == 0); - - nft1155.mint(buyer); - - (ethPrice, currencyPrice) = erc721GatedDiscount.productPrice(slicerId, productId, USDC, quantity, buyer, ""); - - assertTrue(currencyPrice == quantity * (basePrice - fixedDiscountOne)); - assertTrue(ethPrice == 0); - } - - function testConfigureProduct__MultipleCurrencies() public { - DiscountParams[] memory discountsOne = new DiscountParams[](1); - DiscountParams[] memory discountsTwo = new DiscountParams[](1); - CurrencyParams[] memory currenciesParams = new CurrencyParams[](2); - - /// set product price with additional custom inputs - discountsOne[0] = DiscountParams({ - nft: address(nftOne), - discount: fixedDiscountOne, - minQuantity: minNftQuantity, - nftType: NFTType.ERC721, - tokenId: 0 - }); - - currenciesParams[0] = CurrencyParams(ETH, basePrice, false, DiscountType.Absolute, discountsOne); - - /// set product price with different discount for different currency - discountsTwo[0] = DiscountParams({ - nft: address(nftOne), - discount: fixedDiscountTwo, - 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)); - - /// check product price for ETH - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - - assertTrue(ethPrice == quantity * (basePrice - fixedDiscountOne)); - assertTrue(currencyPrice == 0); - - /// check product price for USDC - (uint256 ethPrice2, uint256 usdcPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, USDC, quantity, buyer, ""); - - assertTrue(ethPrice2 == 0); - assertTrue(usdcPrice == (quantity * basePrice) - fixedDiscountTwo); - } - - function testProductPrice__NotNFTOwner() public { - DiscountParams[] memory discounts = new DiscountParams[](1); - - /// set product price for NFT that is not owned by buyer - discounts[0] = DiscountParams({ - nft: address(nftTwo), - 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); - assertTrue(currencyPrice == 0); - } - - function testProductPrice__MinQuantity() public { - DiscountParams[] memory discounts = new DiscountParams[](1); - - /// Buyer owns 1 NFT, but minQuantity is 2 - discounts[0] = DiscountParams({ - nft: address(nftOne), - discount: fixedDiscountOne, - minQuantity: minNftQuantity + 1, - 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); - assertTrue(currencyPrice == 0); - - /// Buyer owns 2 NFTs, minQuantity is 2 - nftOne.mint(buyer); - - /// check product price - (uint256 secondEthPrice, uint256 secondCurrencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - - 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); - } - - function testProductPrice__MultipleBoughtQuantity() 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)); - - // buy multiple products - quantity = 6; - - /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - - assertTrue(ethPrice == quantity * (basePrice - (basePrice * percentDiscount) / 1e4)); - assertTrue(currencyPrice == 0); - } - - function testConfigureProduct__Edit_Add() public { - DiscountParams[] memory discounts = new DiscountParams[](1); - - discounts[0] = DiscountParams({ - nft: address(nftTwo), - discount: fixedDiscountTwo, - minQuantity: minNftQuantity, - nftType: NFTType.ERC721, - tokenId: 0 - }); - - createDiscount(discounts); - - // mint NFT 2 - nftTwo.mint(buyer); - - /// check product price - (uint256 ethPrice, uint256 currencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - - assertTrue(ethPrice == quantity * (basePrice - fixedDiscountTwo)); - assertTrue(currencyPrice == 0); - - discounts = new DiscountParams[](2); - - /// edit product price, with more NFTs and first NFT has higher discount but buyer owns only the second - discounts[0] = DiscountParams({ - nft: address(nftThree), - discount: fixedDiscountOne + 10, - 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 secondEthPrice, uint256 secondCurrencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - - assertTrue(secondEthPrice == quantity * (basePrice - fixedDiscountOne)); - assertTrue(secondCurrencyPrice == 0); - } - - function testConfigureProduct__Edit_Remove() public { - DiscountParams[] memory discounts = 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({ - nft: address(nftThree), - discount: fixedDiscountOne + 10, - 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 secondEthPrice, uint256 secondCurrencyPrice) = - erc721GatedDiscount.productPrice(slicerId, productId, ETH, quantity, buyer, ""); - - assertTrue(secondEthPrice == quantity * (basePrice - fixedDiscountOne)); - assertTrue(secondCurrencyPrice == 0); - - discounts = new DiscountParams[](1); - - discounts[0] = DiscountParams({ - nft: address(nftTwo), - discount: fixedDiscountTwo, - 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 - fixedDiscountTwo)); - assertTrue(currencyPrice == 0); - } -} 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..0cf6ad7 100644 --- a/test/utils/HookTest.sol +++ b/test/utils/HookTest.sol @@ -2,12 +2,13 @@ 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 { 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/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..38e19b6 --- /dev/null +++ b/test/utils/RegistryProductPriceActionTest.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {HookRegistryTest} from "./HookRegistryTest.sol"; +import { + RegistryProductPriceAction, + 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..1e93bae --- /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 {RegistryProductPrice, 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/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); diff --git a/test/utils/mocks/MockProductsModule.sol b/test/utils/mocks/MockProductsModule.sol index 335f87a..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 @@ -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; 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; + } + } +}