diff --git a/.env.example b/.env.example index 8299339..c9a9206 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,4 @@ RPC_URL_BASE_GOERLI= ETHERSCAN_KEY= -PRIVATE_KEY= -SALT_VRGDA_LOGISTIC= -SALT_VRGDA_LINEAR= \ No newline at end of file +PRIVATE_KEY= \ No newline at end of file diff --git a/.gas-snapshot b/.gas-snapshot index d020f7d..d7386f6 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,46 +1,47 @@ -KeysDiscountTest:testDeploy() (gas: 5191) -KeysDiscountTest:testProductPrice__HigherDiscount() (gas: 223239) -KeysDiscountTest:testProductPrice__MinQuantity() (gas: 170349) -KeysDiscountTest:testProductPrice__MultipleBoughtQuantity() (gas: 114430) -KeysDiscountTest:testProductPrice__NotOwner() (gas: 109128) -KeysDiscountTest:testProductPrice__Relative() (gas: 111512) -KeysDiscountTest:testSetProductPrice__ERC20() (gas: 110998) -KeysDiscountTest:testSetProductPrice__ETH() (gas: 110988) -KeysDiscountTest:testSetProductPrice__Edit_Add() (gas: 241504) -KeysDiscountTest:testSetProductPrice__Edit_Remove() (gas: 222224) -KeysDiscountTest:testSetProductPrice__MultipleCurrencies() (gas: 194874) -LinearVRGDATest:testAlwaystargetPriceInRightConditions(uint256) (runs: 258, μ: 14476, ~: 14246) -LinearVRGDATest:testPricingAdjustedByQuantity() (gas: 18812) -LinearVRGDATest:testPricingBasic() (gas: 10483) -LinearVRGDATest:testPricingMin() (gas: 10447) -LinearVRGDATest:testProductPriceErc20() (gas: 24180) -LinearVRGDATest:testProductPriceEth() (gas: 24190) -LinearVRGDATest:testProductPriceMultiple() (gas: 27503) -LinearVRGDATest:testSetMultiplePrices() (gas: 145200) -LinearVRGDATest:testTargetPrice() (gas: 11316) -LogisticVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 258, μ: 16740, ~: 16910) -LogisticVRGDATest:testGetTargetSaleTimeDoesNotRevertEarly() (gas: 6589) -LogisticVRGDATest:testGetTargetSaleTimeRevertsWhenExpected() (gas: 8982) -LogisticVRGDATest:testNoOverflowForAllTokens(uint256,uint256) (runs: 258, μ: 13352, ~: 13593) -LogisticVRGDATest:testNoOverflowForMostTokens(uint256,uint256) (runs: 258, μ: 13157, ~: 13179) -LogisticVRGDATest:testPricingAdjustedByQuantity() (gas: 23932) -LogisticVRGDATest:testPricingBasic() (gas: 11432) -LogisticVRGDATest:testPricingMin() (gas: 11408) -LogisticVRGDATest:testProductPriceErc20() (gas: 26192) -LogisticVRGDATest:testProductPriceEth() (gas: 26203) -LogisticVRGDATest:testProductPriceMultiple() (gas: 32117) -LogisticVRGDATest:testSetMultiplePrices() (gas: 153283) -LogisticVRGDATest:testTargetPrice() (gas: 13278) -LogisticVRGDATest:test_RevertOverflow_BeyondLimitTokens(uint256,uint256) (runs: 258, μ: 14917, ~: 14902) -NFTDiscountTest:testDeploy() (gas: 5191) -NFTDiscountTest:testProductPrice__HigherDiscount() (gas: 222739) -NFTDiscountTest:testProductPrice__MinQuantity() (gas: 150653) -NFTDiscountTest:testProductPrice__MultipleBoughtQuantity() (gas: 114030) -NFTDiscountTest:testProductPrice__NotNFTOwner() (gas: 110876) -NFTDiscountTest:testProductPrice__Relative() (gas: 111113) -NFTDiscountTest:testSetProductPrice__ERC1155() (gas: 161035) -NFTDiscountTest:testSetProductPrice__ERC20() (gas: 110588) -NFTDiscountTest:testSetProductPrice__ETH() (gas: 110611) -NFTDiscountTest:testSetProductPrice__Edit_Add() (gas: 243504) -NFTDiscountTest:testSetProductPrice__Edit_Remove() (gas: 224193) -NFTDiscountTest:testSetProductPrice__MultipleCurrencies() (gas: 194120) \ No newline at end of file +LinearVRGDACorrectnessTest:testParamsSchema() (gas: 9581) +LinearVRGDACorrectnessTest:testSetup_HookInitialized() (gas: 5671) +LinearVRGDACorrectnessTest:testSupportsInterface_RegistryPricingStrategy() (gas: 9685) +LinearVRGDATest:testAlwaystargetPriceInRightConditions(uint256) (runs: 256, μ: 14604, ~: 14369) +LinearVRGDATest:testParamsSchema() (gas: 9564) +LinearVRGDATest:testPricingAdjustedByQuantity() (gas: 18788) +LinearVRGDATest:testPricingBasic() (gas: 10478) +LinearVRGDATest:testPricingMin() (gas: 10499) +LinearVRGDATest:testProductPriceErc20() (gas: 26425) +LinearVRGDATest:testProductPriceEth() (gas: 26435) +LinearVRGDATest:testProductPriceMultiple() (gas: 29695) +LinearVRGDATest:testSetMultiplePrices() (gas: 171215) +LinearVRGDATest:testSetup_HookInitialized() (gas: 5715) +LinearVRGDATest:testSupportsInterface_RegistryPricingStrategy() (gas: 9684) +LinearVRGDATest:testTargetPrice() (gas: 11418) +LogisticVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 16809, ~: 16994) +LogisticVRGDATest:testGetTargetSaleTimeDoesNotRevertEarly() (gas: 6630) +LogisticVRGDATest:testGetTargetSaleTimeRevertsWhenExpected() (gas: 8997) +LogisticVRGDATest:testNoOverflowForAllTokens(uint256,uint256) (runs: 256, μ: 13361, ~: 13589) +LogisticVRGDATest:testNoOverflowForMostTokens(uint256,uint256) (runs: 256, μ: 13212, ~: 13240) +LogisticVRGDATest:testParamsSchema() (gas: 9542) +LogisticVRGDATest:testPricingAdjustedByQuantity() (gas: 23930) +LogisticVRGDATest:testPricingBasic() (gas: 11473) +LogisticVRGDATest:testPricingMin() (gas: 11482) +LogisticVRGDATest:testProductPriceErc20() (gas: 28392) +LogisticVRGDATest:testProductPriceEth() (gas: 28425) +LogisticVRGDATest:testProductPriceMultiple() (gas: 34309) +LogisticVRGDATest:testSetMultiplePrices() (gas: 181254) +LogisticVRGDATest:testSetup_HookInitialized() (gas: 5693) +LogisticVRGDATest:testSupportsInterface_RegistryPricingStrategy() (gas: 9752) +LogisticVRGDATest:testTargetPrice() (gas: 13363) +LogisticVRGDATest:test_RevertOverflow_BeyondLimitTokens(uint256,uint256) (runs: 256, μ: 14963, ~: 14978) +NFTDiscountTest:testDeploy() (gas: 5246) +NFTDiscountTest:testParamsSchema() (gas: 9870) +NFTDiscountTest:testProductPrice__HigherDiscount() (gas: 226170) +NFTDiscountTest:testProductPrice__MinQuantity() (gas: 153567) +NFTDiscountTest:testProductPrice__MultipleBoughtQuantity() (gas: 116886) +NFTDiscountTest:testProductPrice__NotNFTOwner() (gas: 113731) +NFTDiscountTest:testProductPrice__Relative() (gas: 113994) +NFTDiscountTest:testSetProductPrice__ERC1155() (gas: 163762) +NFTDiscountTest:testSetProductPrice__ERC20() (gas: 113514) +NFTDiscountTest:testSetProductPrice__ETH() (gas: 113470) +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 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6037106..25c3e34 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,7 +6,9 @@ jobs: tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + with: + submodules: recursive - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 @@ -14,16 +16,18 @@ jobs: version: stable - name: Install dependencies - run: forge install + run: forge soldeer install - - name: Check contract sizes - run: forge build --sizes + - name: Show Forge version + run: forge --version - - name: Check gas snapshots - run: forge snapshot --check + - name: Build + run: forge build + env: + FOUNDRY_PROFILE: ci - name: Run tests - run: forge test -vvv + run: forge test --isolate -vvv env: - # Only fuzz intensely if we're running this action on a push to main or for a PR going into main: - FOUNDRY_PROFILE: ${{ (github.ref == 'refs/heads/main' || github.base_ref == 'main') && 'intense' }} + FOUNDRY_PROFILE: ci + FORGE_SNAPSHOT_CHECK: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2ab9d62..4453931 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,9 @@ broadcast/ cache/ out/ .env - __pycache__ - # Soldeer /dependencies +remappings.txt +src/**/internal/ \ No newline at end of file diff --git a/LICENSE b/LICENSE index c52d58a..100de61 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Paradigm +Copyright (c) 2022 Slice Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 97cea68..5b8e76d 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,90 @@ -# Slice pricing strategies +# Slice Hooks -This repo contains custom pricing strategies for products sold on [Slice](https://slice.so). +Smart contracts for creating custom pricing strategies and onchain actions for [Slice](https://slice.so) products. Hooks enable dynamic pricing, purchase restrictions, rewards, and other custom behaviors when products are bought. -Each strategy inherits the [ISliceProductPrice](/src/Slice/interfaces/utils/ISliceProductPrice.sol) interface and serves two main purposes: +## Repository Structure -- Allow a product owner to set price params for a product via `setProductPrice`; -- Return product price via `productPrice`; +``` +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 +``` -## Strategies +## Core Concepts -### VRGDA +Slice hooks are built around three main interfaces: -Variable Rate Gradual Dutch Auctions. Read the [whitepaper here](https://www.paradigm.xyz/2022/08/vrgda). +- **[`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 -Slice-specific implementations modified from https://github.com/transmissions11/VRGDAs: +## Hook Types -- [Linear VRGDA](/src/VRGDA/LinearVRGDAPrices.sol) -- [Logistic VRGDA](/src/VRGDA/LogisticVRGDAPrices.sol) +### Registry Hooks (Reusable) -### ERC721 Gated Discount +Deploy once, use across multiple products with frontend integration: -A discount strategy that allows a product owner to set a discount for a product if the buyer owns a specific ERC721 token. +- **[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 -- [ERC721 Gated Discount](/src/ERC721GatedDiscount/ERC721GatedDiscount.sol) +### Product-Specific Hooks -## Contributing +Tailored implementations for individual products: + +- **[Examples](./src/examples/)**: See real-world implementations and creation guide + +## Base Contracts + +### Registry (Reusable): + +- **`RegistryOnchainAction`**: Base for reusable onchain actions +- **`RegistryPricingStrategy`**: Base for reusable pricing strategies +- **`RegistryPricingStrategyAction`**: Base for combined pricing + action hooks + +### Product-Specific + +- **`OnchainAction`**: Base for simple onchain actions +- **`PricingStrategy`**: Base for simple pricing strategies +- **`PricingStrategyAction`**: Base for combined hooks + +## Quick Start + +- **For reusable actions**: See detailed guides in [`/src/hooks/actions`](./src/hooks/actions) +- **For reusable pricing strategies**: See detailed guides in [`/src/hooks/pricing`](./src/hooks/pricing) +- **For reusable pricing strategy actions**: See detailed guides in [`/src/hooks/pricingActions`](./src/hooks/pricingActions) +- **For product-specific hooks**: See implementation examples in [`/src/examples/`](./src/examples/) -```sh -cp .env.example .env -forge install +## Development + +```bash +forge soldeer install # Install dependencies +forge test # Run tests +forge build # Build ``` -You will need a copy of [Foundry](https://github.com/foundry-rs/foundry) installed before proceeding. See the [installation guide](https://github.com/foundry-rs/foundry#installation) for details. +Requires [Foundry](https://book.getfoundry.sh/getting-started/installation). + +## Integration + +Registry hooks automatically integrate with Slice frontends through the `IHookRegistry` interface. + +Product-specific can be attached via the `custom` pricing strategy / onchain action, by passing the deployment address. + +## Testing + +## Contributing + + + \ No newline at end of file diff --git a/TODO.md b/TODO.md deleted file mode 100644 index df616f6..0000000 --- a/TODO.md +++ /dev/null @@ -1,7 +0,0 @@ -- [ ] Figure out where to store slice interface contracts -- Move slice repos in a separate repo + publish to soldeer to import - -- [ ] script to get started quickly - -- [ ] Create a simple cli generator to let the user create a new pricing strategy skeleton easily - -- [ ] improve readme and fix links diff --git a/deployments/addresses.json b/deployments/addresses.json index 430ce1e..fe65f1e 100644 --- a/deployments/addresses.json +++ b/deployments/addresses.json @@ -1,90 +1,5 @@ { - "NFTDiscount": { - "addresses": [ - { - "abiProductPriceSet": { - "type": "event", - "name": "ProductPriceSet", - "inputs": [ - { - "name": "slicerId", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "productId", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "params", - "type": "tuple[]", - "indexed": false, - "internalType": "struct CurrencyParams[]", - "components": [ - { - "name": "currency", - "type": "address", - "internalType": "address" - }, - { - "name": "basePrice", - "type": "uint240", - "internalType": "uint240" - }, - { - "name": "isFree", - "type": "bool", - "internalType": "bool" - }, - { - "name": "discountType", - "type": "uint8", - "internalType": "enum DiscountType" - }, - { - "name": "discounts", - "type": "tuple[]", - "internalType": "struct DiscountParams[]", - "components": [ - { - "name": "nft", - "type": "address", - "internalType": "address" - }, - { - "name": "discount", - "type": "uint80", - "internalType": "uint80" - }, - { - "name": "minQuantity", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "nftType", - "type": "uint8", - "internalType": "enum NFTType" - }, - { - "name": "tokenId", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ] - } - ], - "anonymous": false - }, - "address": "0x3bF8F042158CDdb57c2FDf5695DB9924edF65B33", - "blockNumber": 31437452, - "transactionHash": "0x45a0f87eca9bab42a174a6a32250b2ff515be2400167e1c9b8e9857ec952ef7d" - } - ] - } + "actions": {}, + "pricingStrategies": {}, + "pricingStrategyActions": {} } diff --git a/foundry.toml b/foundry.toml index d780398..531376e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,12 +4,19 @@ optimizer = true optimizer_runs = 1000000 no_match_test = "FFI" dynamic_test_linking = true -libs = ["dependencies"] +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/", + "@openzeppelin-4.8.0/=dependencies/@openzeppelin-contracts-4.8.0/", + "@erc721a/=dependencies/erc721a-4.3.0/contracts/", + "forge-std/=dependencies/forge-std-1.9.7/src/", + "@test/=test/", + "@/=src/" +] -[profile.intense] +[profile.ci] fuzz_runs = 10000 -no_match_test = "FFI" [profile.ffi] ffi = true @@ -17,6 +24,7 @@ match_test = "FFI" no_match_test = "a^" fuzz_runs = 1000 + [rpc_endpoints] mainnet = "${RPC_URL_MAINNET}" op = "${RPC_URL_OPTIMISM}" @@ -31,8 +39,12 @@ base-goerli = {key="${ETHERSCAN_KEY}", chain=84531, url="https://api-goerli.base [soldeer] recursive_deps = true +remappings_generate = false +remappings_regenerate = false [dependencies] +slice = "0.0.4" forge-std = "1.9.7" -"@openzeppelin-contracts" = "5.3.0" -"@openzeppelin-contracts-upgradeable" = "5.3.0" +"@openzeppelin-contracts" = "4.8.0" +erc721a = "4.3.0" + diff --git a/remappings.txt b/remappings.txt deleted file mode 100644 index 2b3ed4f..0000000 --- a/remappings.txt +++ /dev/null @@ -1,3 +0,0 @@ -@openzeppelin-upgradeable/=dependencies/@openzeppelin-contracts-upgradeable-5.3.0/ -@openzeppelin/=dependencies/@openzeppelin-contracts-5.3.0/ -forge-std/=dependencies/forge-std-1.9.7/src/ diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 5e6b27e..2335a30 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -1,9 +1,9 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; -import {BaseScript, SetUpContractsList} from "../utils/ScriptUtils.sol"; import {console} from "forge-std/console.sol"; import {VmSafe} from "forge-std/Vm.sol"; +import {BaseScript, SetUpContractsList} from "./ScriptUtils.sol"; contract DeployScript is BaseScript, SetUpContractsList { constructor() SetUpContractsList("src") {} @@ -18,14 +18,14 @@ contract DeployScript is BaseScript, SetUpContractsList { } function _promptContractName() internal returns (string memory contractName) { - string memory prompt = "\nPricing strategies available to deploy:\n"; - string memory lastFolder = ""; + string memory prompt = "\nContracts available to deploy:\n"; + string memory lastTopFolder = ""; for (uint256 i = 0; i < contractNames.length; i++) { - string memory folder = _getFolderName(contractNames[i].path); - if (i == 0 || keccak256(bytes(folder)) != keccak256(bytes(lastFolder))) { - prompt = string.concat(prompt, "\n"); - prompt = string.concat(prompt, folder, "\n"); - lastFolder = folder; + (string memory topFolderName,) = _getFolderName(contractNames[i].path); + // Print top-level folder if changed + if (i == 0 || keccak256(bytes(topFolderName)) != keccak256(bytes(lastTopFolder))) { + prompt = string.concat(prompt, "\n", topFolderName, "\n"); + lastTopFolder = topFolderName; } prompt = string.concat(prompt, " ", vm.toString(contractNames[i].id), ") ", contractNames[i].name, "\n"); } diff --git a/utils/ScriptUtils.sol b/script/ScriptUtils.sol similarity index 62% rename from utils/ScriptUtils.sol rename to script/ScriptUtils.sol index 607784b..9dba9e3 100644 --- a/utils/ScriptUtils.sol +++ b/script/ScriptUtils.sol @@ -1,12 +1,12 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; import {console} from "forge-std/console.sol"; import {Script} from "forge-std/Script.sol"; -import {ISliceCore} from "./Slice/interfaces/ISliceCore.sol"; -import {IProductsModule} from "./Slice/interfaces/IProductsModule.sol"; -import {IFundsModule} from "./Slice/interfaces/IFundsModule.sol"; 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"; /** * Helper contract to enforce correct chain selection in scripts @@ -82,7 +82,6 @@ abstract contract SetUpContractsList is Script { } struct ContractDeploymentData { - string abi; address contractAddress; uint256 blockNumber; bytes32 transactionHash; @@ -104,9 +103,54 @@ abstract contract SetUpContractsList is Script { _recordContractsOnPath(CONTRACT_PATH); } + function _updateGroupJson( + string memory existingAddresses, + string memory firstFolder, + string memory contractName, + string[] memory json + ) internal returns (string memory) { + string memory groupKey = string.concat(".", firstFolder); + string memory result; + + if (vm.keyExistsJson(existingAddresses, groupKey)) { + // For each contract in contractNames, if it belongs to this group, add its array + for (uint256 i = 0; i < contractNames.length; i++) { + (string memory folderName,) = _getFolderName(contractNames[i].path); + if (keccak256(bytes(folderName)) == keccak256(bytes(firstFolder))) { + string memory name = contractNames[i].name; + if (keccak256(bytes(name)) == keccak256(bytes(contractName))) { + // Use the new json for the contract being updated + result = vm.serializeString(contractName, name, json); + } else { + string memory contractKey = string.concat(".", firstFolder, ".", name); + if (vm.keyExistsJson(existingAddresses, contractKey)) { + // Use the existing array for other contracts + bytes memory arr = vm.parseJson(existingAddresses, contractKey); + ContractDeploymentData[] memory existingData = abi.decode(arr, (ContractDeploymentData[])); + + // Convert to string array format (similar to _buildJsonArray logic) + string[] memory arrStrings = new string[](existingData.length); + for (uint256 j = 0; j < existingData.length; j++) { + string memory idx = vm.toString(j); + vm.serializeAddress(idx, "address", existingData[j].contractAddress); + vm.serializeUint(idx, "blockNumber", existingData[j].blockNumber); + arrStrings[j] = + vm.serializeBytes32(idx, "transactionHash", existingData[j].transactionHash); + } + result = vm.serializeString(contractName, name, arrStrings); + } + } + } + } + return result; + } else { + // If the group doesn't exist, just create a new entry for contractName + return vm.serializeString(contractName, contractName, json); + } + } + function writeAddressesJson(string memory contractName) public { string memory existingAddresses = vm.readFile(ADDRESSES_PATH); - string memory newAddresses = "addresses"; Receipt[] memory receipts = _readReceipts(LAST_TX_PATH); Tx1559[] memory transactions = _readTx1559s(LAST_TX_PATH); @@ -127,7 +171,6 @@ abstract contract SetUpContractsList is Script { return; } - // TODO: Retrieve abi from contract ContractMap memory contractMap; for (uint256 i = 0; i < contractNames.length; i++) { if (keccak256(bytes(contractNames[i].name)) == keccak256(bytes(contractName))) { @@ -135,20 +178,37 @@ abstract contract SetUpContractsList is Script { break; } } - string memory abiPath = string.concat("./out/", contractName, ".sol/ProductPriceSet.json"); - string memory abiValue = vm.readFile(abiPath); - string memory key = string.concat(".", contractName, ".addresses"); - string memory addresses; + // Get the first-level and last folder name + (string memory firstFolder,) = _getFolderName(contractMap.path); + + string[] memory json = + _buildJsonArray(existingAddresses, string.concat(".", firstFolder, ".", contractName), transaction, receipt); + + // Copy all existing top-level groups + vm.serializeJson("addresses", existingAddresses); + + // Update the specific group with the new contract data + string memory updatedGroupJson = _updateGroupJson(existingAddresses, firstFolder, contractName, json); + + // Write the complete JSON with the updated group + vm.writeJson(vm.serializeString("addresses", firstFolder, updatedGroupJson), ADDRESSES_PATH); + } + + function _buildJsonArray( + string memory existingAddresses, + string memory key, + Tx1559 memory transaction, + Receipt memory receipt + ) internal returns (string[] memory json) { if (vm.keyExistsJson(existingAddresses, key)) { // Append new data to existingAddresses bytes memory contractAddressesJson = vm.parseJson(existingAddresses, key); ContractDeploymentData[] memory existingContractAddresses = abi.decode(contractAddressesJson, (ContractDeploymentData[])); - string[] memory json = new string[](existingContractAddresses.length + 1); + json = new string[](existingContractAddresses.length + 1); vm.serializeAddress("0", "address", transaction.contractAddress); - vm.serializeString("0", "abiProductPriceSet", abiValue); vm.serializeUint("0", "blockNumber", receipt.blockNumber); json[0] = vm.serializeBytes32("0", "transactionHash", transaction.hash); @@ -157,31 +217,83 @@ abstract contract SetUpContractsList is Script { string memory index = vm.toString(i + 1); vm.serializeAddress(index, "address", existingContractAddress.contractAddress); - vm.serializeString(index, "abiProductPriceSet", existingContractAddress.abi); vm.serializeUint(index, "blockNumber", existingContractAddress.blockNumber); json[i + 1] = vm.serializeBytes32(index, "transactionHash", existingContractAddress.transactionHash); } - - addresses = vm.serializeString("addresses", "addresses", json); } else { - string[] memory json = new string[](1); - vm.serializeAddress(contractName, "address", transaction.contractAddress); - vm.serializeString(contractName, "abiProductPriceSet", abiValue); - vm.serializeUint(contractName, "blockNumber", receipt.blockNumber); - json[0] = vm.serializeBytes32(contractName, "transactionHash", transaction.hash); - addresses = vm.serializeString("addresses", "addresses", json); + json = new string[](1); + vm.serializeAddress("0", "address", transaction.contractAddress); + vm.serializeUint("0", "blockNumber", receipt.blockNumber); + json[0] = vm.serializeBytes32("0", "transactionHash", transaction.hash); + } + } + + // Helper to check if a path is or is under a 'utils' folder + function _isExcludedPath(string memory path) internal pure returns (bool) { + bytes memory pathBytes = bytes(path); + bytes memory utilsBytes = bytes("/utils"); + for (uint256 i = 0; i + utilsBytes.length <= pathBytes.length; i++) { + bool matchFound = true; + for (uint256 j = 0; j < utilsBytes.length; j++) { + if (pathBytes[i + j] != utilsBytes[j]) { + matchFound = false; + break; + } + } + if (matchFound) { + uint256 afterIdx = i + utilsBytes.length; + if (afterIdx == pathBytes.length || pathBytes[afterIdx] == 0x2f) { + return true; + } + } } - vm.serializeJson(newAddresses, existingAddresses); - newAddresses = vm.serializeString(newAddresses, contractName, addresses); + return false; + } - vm.writeJson(newAddresses, ADDRESSES_PATH); + // Helper to get the last segment of a path (folder or file name) + function _getLastPathSegment(string memory path) internal pure returns (string memory) { + bytes memory pathBytes = bytes(path); + uint256 lastSlash = 0; + for (uint256 i = 0; i < pathBytes.length; i++) { + if (pathBytes[i] == 0x2f) { + // '/' + lastSlash = i + 1; + } + } + if (lastSlash >= pathBytes.length) return ""; + bytes memory segment = new bytes(pathBytes.length - lastSlash); + for (uint256 i = 0; i < segment.length; i++) { + segment[i] = pathBytes[lastSlash + i]; + } + return string(segment); } function _recordContractsOnPath(string memory path) internal { + // Exclude any path that is or is under a 'utils' folder + if (_isExcludedPath(path)) { + return; + } VmSafe.DirEntry[] memory files = vm.readDir(path); + bool isTopLevel = keccak256(bytes(path)) == keccak256(bytes(CONTRACT_PATH)); for (uint256 i = 0; i < files.length; i++) { VmSafe.DirEntry memory file = files[i]; + // Exclude any file or directory under a 'utils' folder + if (_isExcludedPath(file.path)) { + continue; + } if (file.isDir) { + if (isTopLevel) { + string memory folderName = _getLastPathSegment(file.path); + // Only include specific top-level folders + if ( + keccak256(bytes(folderName)) != keccak256(bytes("internal")) + && keccak256(bytes(folderName)) != keccak256(bytes("actions")) + && keccak256(bytes(folderName)) != keccak256(bytes("pricingStrategies")) + && keccak256(bytes(folderName)) != keccak256(bytes("pricingStrategyActions")) + ) { + continue; + } + } _recordContractsOnPath(file.path); } else if (_endsWith(file.path, ".sol")) { string memory content = vm.readFile(file.path); @@ -280,27 +392,65 @@ abstract contract SetUpContractsList is Script { return true; } - function _getFolderName(string memory path) internal view returns (string memory folderName) { + function _getFolderName(string memory path) + internal + view + returns (string memory firstFolderName, string memory lastFolderName) + { bytes memory pathBytes = bytes(path); uint256 lastSlash = 0; uint256 prevSlash = 0; + uint256 srcIndex = 0; + bool foundSrc = false; + // Find the index of '/src/' if present + for (uint256 i = 0; i < pathBytes.length - 3; i++) { + if ( + pathBytes[i] == 0x2f // '/' + && pathBytes[i + 1] == 0x73 // 's' + && pathBytes[i + 2] == 0x72 // 'r' + && pathBytes[i + 3] == 0x63 // 'c' + && (i + 4 == pathBytes.length || pathBytes[i + 4] == 0x2f) // '/' or end + ) { + srcIndex = i + 4; // index after '/src/' + foundSrc = true; + break; + } + } + // Find the first folder after src (or after root if no src) + uint256 start = foundSrc ? srcIndex : 0; + // skip leading slashes + while (start < pathBytes.length && pathBytes[start] == 0x2f) { + start++; + } + uint256 end = start; + while (end < pathBytes.length && pathBytes[end] != 0x2f) { + end++; + } + if (end > start) { + bytes memory firstFolderBytes = new bytes(end - start); + for (uint256 i = 0; i < end - start; i++) { + firstFolderBytes[i] = pathBytes[start + i]; + } + firstFolderName = string(firstFolderBytes); + } else { + firstFolderName = CONTRACT_PATH; + } + // Now get the last folder as before for (uint256 i = 0; i < pathBytes.length; i++) { if (pathBytes[i] == "/") { prevSlash = lastSlash; lastSlash = i; } } - // If only one slash, return the first folder after src - if (lastSlash == 0) return CONTRACT_PATH; - // Find the folder name (between prevSlash and lastSlash) - uint256 start = prevSlash == 0 ? 0 : prevSlash + 1; - uint256 len = lastSlash - start; - if (len == 0) return CONTRACT_PATH; - bytes memory folderBytes = new bytes(len); - for (uint256 i = 0; i < len; i++) { - folderBytes[i] = pathBytes[start + i]; + if (lastSlash == 0) return (firstFolderName, CONTRACT_PATH); + uint256 lastStart = prevSlash == 0 ? 0 : prevSlash + 1; + uint256 lastLen = lastSlash - lastStart; + if (lastLen == 0) return (firstFolderName, CONTRACT_PATH); + bytes memory lastFolderBytes = new bytes(lastLen); + for (uint256 i = 0; i < lastLen; i++) { + lastFolderBytes[i] = pathBytes[lastStart + i]; } - folderName = string(folderBytes); + lastFolderName = string(lastFolderBytes); } // modified from `vm.readTx1559s` to read directly from broadcast artifact diff --git a/script/WriteAddresses.s.sol b/script/WriteAddresses.s.sol index 49b7f65..bddbafc 100644 --- a/script/WriteAddresses.s.sol +++ b/script/WriteAddresses.s.sol @@ -1,7 +1,7 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; -import {SetUpContractsList} from "../utils/ScriptUtils.sol"; +import {SetUpContractsList} from "./ScriptUtils.sol"; contract WriteAddressesScript is SetUpContractsList { constructor() SetUpContractsList("src") {} diff --git a/script/deploy.sh b/script/deploy.sh index bb7258c..71f9d6e 100755 --- a/script/deploy.sh +++ b/script/deploy.sh @@ -11,26 +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 -OUT_DIR="./out/${contractName}.sol" -ARTIFACT="${OUT_DIR}/${contractName}.json" -TARGET_EVENT="ProductPriceSet" -EVENT_JSON="${OUT_DIR}/${TARGET_EVENT}.json" - -# 1. Check if artifact exists -if [ ! -f "$ARTIFACT" ]; then - echo "Artifact not found: $ARTIFACT" - exit 1 -fi - -# 2. Extract the ABI element with name == "ProductPriceSet" -jq '.abi[] | select(.name == "'"$TARGET_EVENT"'")' "$ARTIFACT" > "$EVENT_JSON" - -if [ ! -s "$EVENT_JSON" ]; then - echo "Event $TARGET_EVENT not found in ABI." - exit 1 -fi - forge script script/WriteAddresses.s.sol --sig "run(string memory contractName)" "$contractName" -echo "Deployed contract: $contractName" -echo "Verify abi in deployments/addresses.json is correct!" \ No newline at end of file +echo "Deployed contract: $contractName" \ No newline at end of file diff --git a/script/setupContractTest.sh b/script/setupContractTest.sh new file mode 100755 index 0000000..71f9d6e --- /dev/null +++ b/script/setupContractTest.sh @@ -0,0 +1,16 @@ +#!/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 92541d3..2b1717e 100644 --- a/soldeer.lock +++ b/soldeer.lock @@ -1,16 +1,16 @@ [[dependencies]] name = "@openzeppelin-contracts" -version = "5.3.0" -url = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts/5_3_0_10-04-2025_10:51:50_contracts.zip" -checksum = "fa2bc3db351137c4d5eb32b738a814a541b78e87fbcbfeca825e189c4c787153" -integrity = "d69addf252dfe0688dcd893a7821cbee2421f8ce53d95ca0845a59530043cfd1" +version = "4.8.0" +url = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts/4_8_0_22-01-2024_13:13:40_contracts.zip" +checksum = "932598a6426b76e315bb9fac536011eb21a76984015efe9e8167c4fc9d7e32a3" +integrity = "954367e8adec93f80c6e795012955706347cdb0359360e7c835e4dd29e5a9c2f" [[dependencies]] -name = "@openzeppelin-contracts-upgradeable" -version = "5.3.0" -url = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts-upgradeable/5_3_0_10-04-2025_10:51:56_contracts-upgradeable.zip" -checksum = "4bd92f87af0cac7226b12ce367e7327f13431735fa6010508d8c8177f9d3d10f" -integrity = "fa195a69ef4dfec7fec7fbbb77f424258c821832fdd355b0a6e5fe34d2986a16" +name = "erc721a" +version = "4.3.0" +url = "https://soldeer-revisions.s3.amazonaws.com/erc721a/4_3_0_14-03-2024_06:28:52_erc721a.zip" +checksum = "95826815148f5281311395186258940cc01f18d5d95975050a2c5f0f835d476d" +integrity = "43c1eb15351e04592630e7200eb7a8eca32aedc86be492d4f0c1825065cd70eb" [[dependencies]] name = "forge-std" @@ -18,3 +18,10 @@ version = "1.9.7" url = "https://soldeer-revisions.s3.amazonaws.com/forge-std/1_9_7_28-04-2025_15:55:08_forge-std-1.9.zip" checksum = "8d9e0a885fa8ee6429a4d344aeb6799119f6a94c7c4fe6f188df79b0dce294ba" integrity = "9e60fdba82bc374df80db7f2951faff6467b9091873004a3d314cf0c084b3c7d" + +[[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" diff --git a/src/TieredDiscount/TieredDiscount.sol b/src/TieredDiscount/TieredDiscount.sol deleted file mode 100644 index 114c9e1..0000000 --- a/src/TieredDiscount/TieredDiscount.sol +++ /dev/null @@ -1,126 +0,0 @@ -// SPDX-License-Identifier: Unlicense -pragma solidity ^0.8.19; - -import {ISliceProductPrice} from "../../utils/Slice/interfaces/utils/ISliceProductPrice.sol"; -import {IProductsModule} from "../../utils/Slice/interfaces/IProductsModule.sol"; -import {CurrencyParams} from "./structs/CurrencyParams.sol"; -import {ProductDiscounts, DiscountType} from "./structs/ProductDiscounts.sol"; -import {DiscountParams, NFTType} from "./structs/DiscountParams.sol"; - -/** - * @notice Slice pricing strategy with discounts based on asset ownership - * @author Dom-Mac <@zerohex_eth> - * @author jacopo <@jj_ranalli> - */ -abstract contract TieredDiscount is ISliceProductPrice { - event ProductPriceSet(uint256 slicerId, uint256 productId, CurrencyParams[] params); - - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - error NotProductOwner(); - error WrongCurrency(); - error InvalidRelativeDiscount(); - error InvalidMinQuantity(); - error DiscountsNotDescending(DiscountParams nft); - - /*////////////////////////////////////////////////////////////// - IMMUTABLE STORAGE - //////////////////////////////////////////////////////////////*/ - - address public immutable productsModuleAddress; - - /*////////////////////////////////////////////////////////////// - MUTABLE STORAGE - //////////////////////////////////////////////////////////////*/ - - mapping(uint256 slicerId => mapping(uint256 productId => mapping(address currency => ProductDiscounts))) public - productDiscounts; - - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - constructor(address _productsModuleAddress) { - productsModuleAddress = _productsModuleAddress; - } - - /*////////////////////////////////////////////////////////////// - MODIFIERS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Check if msg.sender is owner of a product. Used to manage access to `setProductPrice`. - */ - modifier onlyProductOwner(uint256 slicerId, uint256 productId) { - if (!IProductsModule(productsModuleAddress).isProductOwner(slicerId, productId, msg.sender)) { - revert NotProductOwner(); - } - _; - } - - /*////////////////////////////////////////////////////////////// - FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Called by product owner to set base price and discounts for a product. - * - * @param slicerId ID of the slicer to set the price params for. - * @param productId ID of the product to set the price params for. - * @param params Array of `CurrencyParams` structs - */ - function setProductPrice(uint256 slicerId, uint256 productId, CurrencyParams[] memory params) - external - onlyProductOwner(slicerId, productId) - { - _setProductPrice(slicerId, productId, params); - emit ProductPriceSet(slicerId, productId, params); - } - - /** - * @notice Function called by Slice protocol to calculate current product price. - * - * @param slicerId ID of the slicer being queried - * @param productId ID of the product being queried - * @param currency Currency chosen for the purchase - * @param quantity Number of units purchased - * @param buyer Address of the buyer - * @param params Additional params used to calculate price - * - * @return ethPrice and currencyPrice of product. - */ - function productPrice( - uint256 slicerId, - uint256 productId, - address currency, - uint256 quantity, - address buyer, - bytes memory params - ) public view override returns (uint256 ethPrice, uint256 currencyPrice) { - ProductDiscounts memory discountParams = productDiscounts[slicerId][productId][currency]; - - if (discountParams.basePrice == 0) { - if (!discountParams.isFree) revert WrongCurrency(); - } else { - return _productPrice(slicerId, productId, currency, quantity, buyer, params, discountParams); - } - } - - /*////////////////////////////////////////////////////////////// - INTERNAL - //////////////////////////////////////////////////////////////*/ - - function _setProductPrice(uint256 slicerId, uint256 productId, CurrencyParams[] memory params) internal virtual; - - function _productPrice( - uint256 slicerId, - uint256 productId, - address currency, - uint256 quantity, - address buyer, - bytes memory params, - ProductDiscounts memory discountParams - ) internal view virtual returns (uint256 ethPrice, uint256 currencyPrice); -} diff --git a/src/brtmoments/BrightMomentsCafe.sol b/src/brtmoments/BrightMomentsCafe.sol deleted file mode 100644 index a44b94f..0000000 --- a/src/brtmoments/BrightMomentsCafe.sol +++ /dev/null @@ -1,142 +0,0 @@ -// SPDX-License-Identifier: Unlicense -pragma solidity ^0.8.19; - -import {ISliceProductPrice} from "../../utils/Slice/interfaces/utils/ISliceProductPrice.sol"; -import {Ownable} from "@openzeppelin/access/Ownable.sol"; -import {IERC721} from "@openzeppelin/token/ERC721/IERC721.sol"; -import {IProductsModule} from "../../utils/Slice/interfaces/IProductsModule.sol"; - -/** - * @notice Slice pricing strategy for bright moments - * @author jacopo <@jj_ranalli> - * @author Dom-Mac <@zerohex_eth> - */ -contract BrightMomentsCafe is ISliceProductPrice, Ownable { - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - error NotProductOwner(); - - /*////////////////////////////////////////////////////////////// - IMMUTABLE STORAGE - //////////////////////////////////////////////////////////////*/ - - address public immutable productsModuleAddress; - uint256 public constant BRTMOMENTSCAFE_STOREID = 298; - uint256 internal constant MAX_PRODUCTID = 8; - - /*////////////////////////////////////////////////////////////// - MUTABLE STORAGE - //////////////////////////////////////////////////////////////*/ - - mapping(uint256 productId => uint256 price) public usdcPrices; - mapping(address => bool) public whitelistedAddresses; - IERC721[] public nfts; - - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - constructor(address _productsModuleAddress) Ownable(msg.sender) { - productsModuleAddress = _productsModuleAddress; - - usdcPrices[1] = 5400000; - usdcPrices[2] = 5400000; - usdcPrices[3] = 5400000; - usdcPrices[4] = 5400000; - usdcPrices[5] = 5400000; - usdcPrices[6] = 5400000; - usdcPrices[7] = 3250000; // tea - usdcPrices[8] = 2150000; // water - usdcPrices[9] = 10800000; // air - usdcPrices[10] = 54000000; // quarterly - usdcPrices[11] = 215000000; // ledger - usdcPrices[12] = 86500000; // hoodie - usdcPrices[13] = 48500000; // shirt - usdcPrices[14] = 48500000; // shirt - usdcPrices[15] = 21500000; // tote - - whitelistedAddresses[0xAe009d532328FF09e09E5d506aB5BBeC3c25c0FF] = true; - whitelistedAddresses[0xf4140f2721f5Fd76eA2A3b6864ab49e0fBa1f7d0] = true; - whitelistedAddresses[0x396D8177e5E1b9cAfb89692261f6c647Aa77f00C] = true; - - nfts.push(IERC721(0xFc30e5Ab92b78928634B4F7C6000F80d700bcE56)); - nfts.push(IERC721(0x55E8749AA336D30DF466078B9535cd97b5024dcf)); - nfts.push(IERC721(0x36FcD1b2CA01aD91D0a0680B4EAc21149F2a98Bb)); - nfts.push(IERC721(0x9D19D7F02AD521A377AafD331cCFF8162Fc52959)); - } - - /*////////////////////////////////////////////////////////////// - FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Called by product owner to set base price and discounts for a product. - * - * @param productId ID of the product to set the price params for. - * @param newPrice New base price for the product. - */ - function setProductPrice(uint256 productId, uint256 newPrice) external onlyOwner { - usdcPrices[productId] = newPrice; - } - - /** - * @notice Called by product owner to set base price and discounts for a product. - * - * @param addr Address to set as whitelisted or not. - * @param isWhitelisted Whether to whitelist or not. - */ - function setWhitelistedAddress(address addr, bool isWhitelisted) external onlyOwner { - whitelistedAddresses[addr] = isWhitelisted; - } - - /** - * @notice Called by product owner to set nfts eligible for free coffee. - * - * @param _nfts Array of NFTs to set. - */ - function setNfts(IERC721[] calldata _nfts) external onlyOwner { - nfts = _nfts; - } - - /** - * @notice Function called by Slice protocol to calculate current product price. - * - * @param productId ID of the product being queried - * @param quantity Number of units purchased - * @param buyer Address of the buyer - * - * @return ethPrice and currencyPrice of product. - */ - function productPrice(uint256, uint256 productId, address, uint256 quantity, address buyer, bytes memory) - public - view - override - returns (uint256 ethPrice, uint256 currencyPrice) - { - if (whitelistedAddresses[buyer]) { - return (0, 0); - } - - if (productId <= MAX_PRODUCTID) { - for (uint256 i = 0; i < nfts.length; ++i) { - if (nfts[i].balanceOf(buyer) != 0) { - for (uint256 k = 1; k <= MAX_PRODUCTID; ++k) { - if ( - IProductsModule(productsModuleAddress).validatePurchaseUnits( - buyer, BRTMOMENTSCAFE_STOREID, k - ) != 0 - ) { - return (0, usdcPrices[productId] * quantity); - } - } - - return (0, usdcPrices[productId] * (quantity - 1)); - } - } - } - - return (0, usdcPrices[productId] * quantity); - } -} diff --git a/src/brtmoments/NFTDiscountFixed.sol b/src/brtmoments/NFTDiscountFixed.sol deleted file mode 100644 index c20cf0c..0000000 --- a/src/brtmoments/NFTDiscountFixed.sol +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: Unlicense -pragma solidity ^0.8.19; - -import {ISliceProductPrice} from "../../utils/Slice/interfaces/utils/ISliceProductPrice.sol"; -import {Ownable} from "@openzeppelin/access/Ownable.sol"; -import {IERC1155} from "@openzeppelin/token/ERC1155/IERC1155.sol"; - -struct Price { - uint128 basePrice; - uint128 discountedPrice; -} - -struct NFT { - IERC1155 nftAddress; - uint256 tokenId; -} - -contract NFTDiscountFixed is ISliceProductPrice, Ownable { - /*////////////////////////////////////////////////////////////// - MUTABLE STORAGE - //////////////////////////////////////////////////////////////*/ - - NFT[] public nfts; - mapping(uint256 productId => Price price) public usdcPrices; - - /*////////////////////////////////////////////////////////////// - FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - constructor() Ownable(msg.sender) { - nfts.push(NFT(IERC1155(0x8876CD7B283b1EeB998aDfeB55a65C51D6b6f693), 1)); - - for (uint256 i = 0; i < 46; i++) { - usdcPrices[i] = Price(14_000_000, 2_000_000); - } - } - - /** - * @notice Called by product owner to set base price and discounts for a product. - * - * @param productId ID of the product to set the price params for. - * @param newPrice New base price for the product. - */ - function setProductPrice(uint256 productId, Price memory newPrice) external onlyOwner { - usdcPrices[productId] = newPrice; - } - - /** - * @notice Called by product owner to set nfts eligible for free coffee. - * - * @param _nfts Array of NFTs to set. - */ - function setNfts(NFT[] memory _nfts) external onlyOwner { - for (uint256 i = 0; i < _nfts.length; i++) { - nfts[i] = _nfts[i]; - } - - for (uint256 i = _nfts.length; i < nfts.length; i++) { - nfts.pop(); - } - } - - /** - * @notice Function called by Slice protocol to calculate current product price. - * - * @param productId ID of the product being queried - * @param quantity Number of units purchased - * @param buyer Address of the buyer - * - * @return ethPrice and currencyPrice of product. - */ - function productPrice(uint256, uint256 productId, address, uint256 quantity, address buyer, bytes memory) - public - view - override - returns (uint256 ethPrice, uint256 currencyPrice) - { - NFT memory nft; - for (uint256 i = 0; i < nfts.length; ++i) { - nft = nfts[i]; - if (nft.nftAddress.balanceOf(buyer, nft.tokenId) != 0) { - return (0, usdcPrices[productId].discountedPrice * quantity); - } - } - - return (0, usdcPrices[productId].basePrice * quantity); - } -} diff --git a/src/brtmoments/OneDiscounted.sol b/src/brtmoments/OneDiscounted.sol deleted file mode 100644 index 0574f98..0000000 --- a/src/brtmoments/OneDiscounted.sol +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-License-Identifier: Unlicense -pragma solidity ^0.8.19; - -import {ISliceProductPrice} from "../../utils/Slice/interfaces/utils/ISliceProductPrice.sol"; -import {IERC721} from "@openzeppelin/token/ERC721/IERC721.sol"; -import {IERC1155} from "@openzeppelin/token/ERC1155/IERC1155.sol"; -import {IProductsModule} from "../../utils/Slice/interfaces/IProductsModule.sol"; - -/** - * @notice Slice pricing strategy to give one product for free - * @author jacopo <@jj_ranalli> - */ -contract OneDiscounted is ISliceProductPrice { - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - error NotProductOwner(); - - /*////////////////////////////////////////////////////////////// - IMMUTABLE STORAGE - //////////////////////////////////////////////////////////////*/ - - address public immutable productsModuleAddress; - - /*////////////////////////////////////////////////////////////// - MUTABLE STORAGE - //////////////////////////////////////////////////////////////*/ - - struct Price { - uint256 usdcPrice; - address token; - uint88 tokenId; - TokenType tokenType; - } - - enum TokenType { - ERC721, - ERC1155 - } - - mapping(uint256 productId => Price price) public usdcPrices; - mapping(address => bool) public whitelistedAddresses; - IERC721[] public nfts; - - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - constructor(address _productsModuleAddress) { - productsModuleAddress = _productsModuleAddress; - } - - /*////////////////////////////////////////////////////////////// - MODIFIERS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Check if msg.sender is owner of a product. Used to manage access to `setProductPrice`. - */ - modifier onlyProductOwner(uint256 slicerId, uint256 productId) { - if (!IProductsModule(productsModuleAddress).isProductOwner(slicerId, productId, msg.sender)) { - revert NotProductOwner(); - } - _; - } - - /*////////////////////////////////////////////////////////////// - FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Called by product owner to set base price and discounts for a product. - * - * @param slicerId ID of the slicer to set the price for. - * @param productId ID of the product to set the price for. - * @param usdcPrice Price of the product in USDC, with 6 decimals. - * @param token Address of the token to hold to obtain a free item. Set to address(0) to bypass check. - * @param tokenId ID of the token to hold to be eligible for discount (only for ERC1155). - * @param tokenType Type of the token to hold to be eligible for discount - 1 for ERC721, 2 for ERC1155. - */ - function setProductPrice( - uint256 slicerId, - uint256 productId, - uint256 usdcPrice, - address token, - uint88 tokenId, - TokenType tokenType - ) external onlyProductOwner(slicerId, productId) { - usdcPrices[productId] = Price(usdcPrice, token, tokenId, tokenType); - } - - /** - * @notice Function called by Slice protocol to calculate current product price. - * Discount is applied only for first purchase on a slicer. - * - * @param productId ID of the product being queried - * @param quantity Number of units purchased - * @param buyer Address of the buyer - * - * @return ethPrice and currencyPrice of product. - */ - function productPrice(uint256 slicerId, uint256 productId, address, uint256 quantity, address buyer, bytes memory) - public - view - override - returns (uint256 ethPrice, uint256 currencyPrice) - { - Price memory price = usdcPrices[productId]; - - bool isEligible = price.token == address(0); - if (!isEligible) { - if (price.tokenType == TokenType.ERC721) { - isEligible = IERC721(price.token).balanceOf(buyer) != 0; - } else { - isEligible = IERC1155(price.token).balanceOf(buyer, price.tokenId) != 0; - } - } - - if (isEligible) { - for (uint256 i = 1; i <= IProductsModule(productsModuleAddress).nextProductId(slicerId); ++i) { - if (IProductsModule(productsModuleAddress).validatePurchaseUnits(buyer, slicerId, i) != 0) { - return (0, usdcPrices[productId].usdcPrice * quantity); - } - } - - return (0, usdcPrices[productId].usdcPrice * (quantity - 1)); - } - - return (0, usdcPrices[productId].usdcPrice * quantity); - } -} diff --git a/src/brtmoments/OneForFree.sol b/src/brtmoments/OneForFree.sol deleted file mode 100644 index fb3d84f..0000000 --- a/src/brtmoments/OneForFree.sol +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-License-Identifier: Unlicense -pragma solidity ^0.8.19; - -import {ISliceProductPrice} from "../../utils/Slice/interfaces/utils/ISliceProductPrice.sol"; -import {IERC721} from "@openzeppelin/token/ERC721/IERC721.sol"; -import {IERC1155} from "@openzeppelin/token/ERC1155/IERC1155.sol"; -import {IProductsModule} from "../../utils/Slice/interfaces/IProductsModule.sol"; - -/** - * @notice Slice pricing strategy to give one product for free - * @author jacopo <@jj_ranalli> - */ -contract OneForFree is ISliceProductPrice { - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - error NotProductOwner(); - - /*////////////////////////////////////////////////////////////// - IMMUTABLE STORAGE - //////////////////////////////////////////////////////////////*/ - - address public immutable productsModuleAddress; - - /*////////////////////////////////////////////////////////////// - MUTABLE STORAGE - //////////////////////////////////////////////////////////////*/ - - struct Price { - uint256 usdcPrice; - address token; - uint88 tokenId; - TokenType tokenType; - } - - enum TokenType { - ERC721, - ERC1155 - } - - mapping(uint256 productId => Price price) public usdcPrices; - mapping(address => bool) public whitelistedAddresses; - IERC721[] public nfts; - - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - constructor(address _productsModuleAddress) { - productsModuleAddress = _productsModuleAddress; - } - - /*////////////////////////////////////////////////////////////// - MODIFIERS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Check if msg.sender is owner of a product. Used to manage access to `setProductPrice`. - */ - modifier onlyProductOwner(uint256 slicerId, uint256 productId) { - if (!IProductsModule(productsModuleAddress).isProductOwner(slicerId, productId, msg.sender)) { - revert NotProductOwner(); - } - _; - } - - /*////////////////////////////////////////////////////////////// - FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Called by product owner to set base price and discounts for a product. - * - * @param slicerId ID of the slicer to set the price for. - * @param productId ID of the product to set the price for. - * @param usdcPrice Price of the product in USDC, with 6 decimals. - * @param token Address of the token to hold to obtain a free item. Set to address(0) to bypass check. - * @param tokenId ID of the token to hold to be eligible for discount (only for ERC1155). - * @param tokenType Type of the token to hold to be eligible for discount - 1 for ERC721, 2 for ERC1155. - */ - function setProductPrice( - uint256 slicerId, - uint256 productId, - uint256 usdcPrice, - address token, - uint88 tokenId, - TokenType tokenType - ) external onlyProductOwner(slicerId, productId) { - usdcPrices[productId] = Price(usdcPrice, token, tokenId, tokenType); - } - - /** - * @notice Function called by Slice protocol to calculate current product price. - * Discount is applied only for first purchase on a slicer. - * - * @param productId ID of the product being queried - * @param quantity Number of units purchased - * @param buyer Address of the buyer - * - * @return ethPrice and currencyPrice of product. - */ - function productPrice(uint256 slicerId, uint256 productId, address, uint256 quantity, address buyer, bytes memory) - public - view - override - returns (uint256 ethPrice, uint256 currencyPrice) - { - Price memory price = usdcPrices[productId]; - - bool isEligible = price.token == address(0); - if (!isEligible) { - if (price.tokenType == TokenType.ERC721) { - isEligible = IERC721(price.token).balanceOf(buyer) != 0; - } else { - isEligible = IERC1155(price.token).balanceOf(buyer, price.tokenId) != 0; - } - } - - if (isEligible) { - for (uint256 i = 1; i <= IProductsModule(productsModuleAddress).nextProductId(slicerId); ++i) { - if (IProductsModule(productsModuleAddress).validatePurchaseUnits(buyer, slicerId, i) != 0) { - return (0, usdcPrices[productId].usdcPrice * quantity); - } - } - - return (0, usdcPrices[productId].usdcPrice * (quantity - 1)); - } - - return (0, usdcPrices[productId].usdcPrice * quantity); - } -} diff --git a/src/examples/README.md b/src/examples/README.md new file mode 100644 index 0000000..6861892 --- /dev/null +++ b/src/examples/README.md @@ -0,0 +1,135 @@ +# Example Implementations + +This folder contains product-specific smart contract implementations that demonstrate how to use Slice hooks for real-world use cases. These examples can be used as templates for creating your own custom implementations, without focusing on reusability and integration with Slice. + +## Key Interfaces + +**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 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 + +### 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"; + +contract MyProductAction is OnchainAction { + constructor(IProductsModule productsModule, uint256 slicerId) + OnchainAction(productsModule, slicerId) {} +} +``` + +2. **Implement required functions**: +```solidity +function _onProductPurchase( + uint256 slicerId, + uint256 productId, + address account, + uint256 quantity, + bytes memory slicerCustomData, + bytes memory buyerCustomData +) internal override { + // Your custom logic here - mint NFTs, track purchases, etc. +} + +// Optional: Add purchase restrictions +function isPurchaseAllowed( + uint256 slicerId, + uint256 productId, + address account, + uint256 quantity, + bytes memory slicerCustomData, + bytes memory buyerCustomData +) public view override returns (bool) { + // Your eligibility logic here +} +``` + +### Pricing Strategy + +To create a custom product-specific pricing strategy: + +1. **Inherit from PricingStrategy**: +```solidity +import {PricingStrategy, IProductsModule} from "@/utils/PricingStrategy.sol"; + +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 +} +``` + +## 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 new file mode 100644 index 0000000..6dcc920 --- /dev/null +++ b/src/examples/actions/BaseCafe_2.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IProductsModule, OnchainAction} from "@/utils/OnchainAction.sol"; + +/** + * @title BaseCafe + * @notice Onchain action that mints an NFT to the buyer on every purchase. + * @author Slice + */ +contract BaseCafe is OnchainAction { + /*////////////////////////////////////////////////////////////// + IMMUTABLE STORAGE + //////////////////////////////////////////////////////////////*/ + + ITokenERC1155 public constant MINT_NFT_COLLECTION = ITokenERC1155(0x8485A580A9975deF42F8C7c5C63E9a0FF058561D); + uint256 public constant MINT_NFT_TOKEN_ID = 9; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(IProductsModule productsModuleAddress, uint256 slicerId) + OnchainAction(productsModuleAddress, slicerId) + {} + + /*////////////////////////////////////////////////////////////// + FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc OnchainAction + * @notice Mint `quantity` NFTs to `account` on purchase + */ + function _onProductPurchase(uint256, uint256, address buyer, uint256 quantity, bytes memory, bytes memory) + internal + override + { + MINT_NFT_COLLECTION.mintTo(buyer, MINT_NFT_TOKEN_ID, "", quantity); + } +} + +interface ITokenERC1155 { + function mintTo(address to, uint256 tokenId, string calldata uri, uint256 amount) external; +} diff --git a/src/examples/actions/BaseGirlsScout.sol b/src/examples/actions/BaseGirlsScout.sol new file mode 100644 index 0000000..a64517e --- /dev/null +++ b/src/examples/actions/BaseGirlsScout.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IProductsModule, OnchainAction} from "@/utils/OnchainAction.sol"; +import {Ownable} from "@openzeppelin-4.8.0/access/Ownable.sol"; +import {IERC1155} from "@openzeppelin-4.8.0/interfaces/IERC1155.sol"; + +/** + * @title BaseGirlsScout + * @notice Onchain action that mints Base Girls Scout NFTs to the buyer on every purchase. + * @author Slice + */ +contract BaseGirlsScout is OnchainAction, Ownable { + /*////////////////////////////////////////////////////////////// + IMMUTABLE STORAGE + //////////////////////////////////////////////////////////////*/ + + ITokenERC1155 public MINT_NFT_COLLECTION = ITokenERC1155(0x7A110890DF5D95CefdB0151143E595b755B7c9b7); + uint256 public MINT_NFT_TOKEN_ID = 1; + + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(uint256 slicerId => bool allowed) public allowedSlicerIds; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(IProductsModule productsModuleAddress, uint256 slicerId) + OnchainAction(productsModuleAddress, slicerId) + Ownable() + { + allowedSlicerIds[2217] = true; + allowedSlicerIds[2218] = true; + } + + /*////////////////////////////////////////////////////////////// + FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc OnchainAction + * @notice Mint `quantity` NFTs to `account` on purchase + */ + function _onProductPurchase(uint256, uint256, address buyer, uint256 quantity, bytes memory, bytes memory) + internal + override + { + MINT_NFT_COLLECTION.mintTo(buyer, MINT_NFT_TOKEN_ID, "", quantity); + } + + /*////////////////////////////////////////////////////////////// + FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Called by contract owner to set allowed slicer Ids. + */ + function setAllowedSlicerId(uint256 slicerId, bool allowed) external onlyOwner { + allowedSlicerIds[slicerId] = allowed; + } + + /** + * @notice Called by contract owner to set the mint token collection and token ID. + */ + function setMintTokenId(address collection, uint256 tokenId) external onlyOwner { + MINT_NFT_COLLECTION = ITokenERC1155(collection); + MINT_NFT_TOKEN_ID = tokenId; + } +} + +interface ITokenERC1155 { + function mintTo(address to, uint256 tokenId, string calldata uri, uint256 amount) external; +} diff --git a/src/hooks/actions/Allowlisted/Allowlisted.sol b/src/hooks/actions/Allowlisted/Allowlisted.sol new file mode 100644 index 0000000..cadb88d --- /dev/null +++ b/src/hooks/actions/Allowlisted/Allowlisted.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {MerkleProof} from "@openzeppelin-4.8.0/utils/cryptography/MerkleProof.sol"; +import { + IProductsModule, + RegistryOnchainAction, + HookRegistry, + IOnchainAction, + IHookRegistry +} from "@/utils/RegistryOnchainAction.sol"; + +/** + * @title Allowlisted + * @notice Onchain action registry for allowlist requirement. + * @author Slice + */ +contract Allowlisted is RegistryOnchainAction { + /*////////////////////////////////////////////////////////////// + MUTABLE STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(uint256 slicerId => mapping(uint256 productId => bytes32 merkleRoot)) public merkleRoots; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(IProductsModule productsModuleAddress) RegistryOnchainAction(productsModuleAddress) {} + + /*////////////////////////////////////////////////////////////// + CONFIGURATION + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IOnchainAction + * @dev Checks if the account is in the allowlist. + */ + function isPurchaseAllowed( + uint256 slicerId, + uint256 productId, + address account, + uint256, + bytes memory, + bytes memory buyerCustomData + ) public view override returns (bool isAllowed) { + // Get Merkle proof from buyerCustomData + bytes32[] memory proof = abi.decode(buyerCustomData, (bytes32[])); + + // Generate leaf from account address + bytes32 leaf = keccak256(abi.encodePacked(account)); + bytes32 root = merkleRoots[slicerId][productId]; + + // Check if Merkle proof is valid + isAllowed = MerkleProof.verify(proof, root, leaf); + } + + /** + * @inheritdoc HookRegistry + * @dev Sets the Merkle root for the allowlist. + */ + function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { + (bytes32 merkleRoot) = abi.decode(params, (bytes32)); + merkleRoots[slicerId][productId] = merkleRoot; + } + + /** + * @inheritdoc IHookRegistry + */ + function paramsSchema() external pure override returns (string memory) { + return "bytes32 merkleRoot"; + } +} diff --git a/src/hooks/actions/ERC20Gated/ERC20Gated.sol b/src/hooks/actions/ERC20Gated/ERC20Gated.sol new file mode 100644 index 0000000..bbc4d4e --- /dev/null +++ b/src/hooks/actions/ERC20Gated/ERC20Gated.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ERC20Gate} from "./types/ERC20Gate.sol"; +import { + IProductsModule, + RegistryOnchainAction, + HookRegistry, + IOnchainAction, + IHookRegistry +} from "@/utils/RegistryOnchainAction.sol"; + +/** + * @title ERC20Gated + * @notice Onchain action registry for ERC20 gating. + * @author Slice + */ +contract ERC20Gated is RegistryOnchainAction { + /*////////////////////////////////////////////////////////////// + MUTABLE STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(uint256 slicerId => mapping(uint256 productId => ERC20Gate[] gates)) public tokenGates; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(IProductsModule productsModuleAddress) RegistryOnchainAction(productsModuleAddress) {} + + /*////////////////////////////////////////////////////////////// + CONFIGURATION + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IOnchainAction + * @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) { + 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); + if (accountBalance < gate.amount) { + return false; + } + } + + return true; + } + + /** + * @inheritdoc HookRegistry + * @dev Sets the ERC20 gates for a product. + */ + function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { + (ERC20Gate[] memory gates) = abi.decode(params, (ERC20Gate[])); + + for (uint256 i = 0; i < gates.length; i++) { + tokenGates[slicerId][productId].push(gates[i]); + } + } + + /** + * @inheritdoc IHookRegistry + */ + function paramsSchema() external pure override returns (string memory) { + return "(address erc20,uint256 amount)[] erc20Gates"; + } +} diff --git a/src/hooks/actions/ERC20Gated/types/ERC20Gate.sol b/src/hooks/actions/ERC20Gated/types/ERC20Gate.sol new file mode 100644 index 0000000..9b75f39 --- /dev/null +++ b/src/hooks/actions/ERC20Gated/types/ERC20Gate.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin-4.8.0/interfaces/IERC20.sol"; + +struct ERC20Gate { + IERC20 erc20; + uint256 amount; +} diff --git a/src/hooks/actions/ERC20Mint/ERC20Mint.sol b/src/hooks/actions/ERC20Mint/ERC20Mint.sol new file mode 100644 index 0000000..e8398ed --- /dev/null +++ b/src/hooks/actions/ERC20Mint/ERC20Mint.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { + IProductsModule, + RegistryOnchainAction, + HookRegistry, + IOnchainAction, + IHookRegistry +} from "@/utils/RegistryOnchainAction.sol"; +import {ERC20Data} from "./types/ERC20Data.sol"; +import {ERC20Mint_BaseToken} from "./utils/ERC20Mint_BaseToken.sol"; + +/** + * @title ERC20Mint + * @notice Onchain action registry for minting ERC20 tokens on every purchase. + * @dev If `revertOnMaxSupplyReached` is set to true, reverts when max supply is exceeded. + * @author Slice + */ +contract ERC20Mint is RegistryOnchainAction { + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error MaxSupplyExceeded(); + error InvalidTokensPerUnit(); + + /*////////////////////////////////////////////////////////////// + MUTABLE STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(uint256 slicerId => mapping(uint256 productId => ERC20Data tokenData)) public tokenData; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(IProductsModule productsModuleAddress) RegistryOnchainAction(productsModuleAddress) {} + + /*////////////////////////////////////////////////////////////// + CONFIGURATION + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IOnchainAction + * @dev If `revertOnMaxSupplyReached` is set to true, returns false when max supply is exceeded. + */ + function isPurchaseAllowed( + uint256 slicerId, + uint256 productId, + address, + uint256 quantity, + bytes memory, + bytes memory + ) public view virtual override returns (bool isAllowed) { + ERC20Data memory tokenData_ = tokenData[slicerId][productId]; + + if (tokenData_.revertOnMaxSupplyReached) { + return + tokenData_.token.totalSupply() + (quantity * tokenData_.tokensPerUnit) <= tokenData_.token.maxSupply(); + } + + return true; + } + + /** + * @inheritdoc RegistryOnchainAction + * @notice Mint tokens to the buyer. + * @dev If `revertOnMaxSupplyReached` is set to true, reverts when max supply is exceeded. + */ + function _onProductPurchase( + uint256 slicerId, + uint256 productId, + address buyer, + uint256 quantity, + bytes memory, + bytes memory + ) internal override { + ERC20Data memory tokenData_ = tokenData[slicerId][productId]; + + uint256 tokensToMint = quantity * tokenData_.tokensPerUnit; + + (bool success,) = + address(tokenData_.token).call(abi.encodeWithSelector(tokenData_.token.mint.selector, buyer, tokensToMint)); + + if (tokenData_.revertOnMaxSupplyReached) { + if (!success) revert MaxSupplyExceeded(); + } + } + + /** + * @inheritdoc HookRegistry + * @dev Set the ERC20 data for a product. + */ + function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { + ( + string memory name, + string memory symbol, + uint256 premintAmount, + address premintReceiver, + bool revertOnMaxSupplyReached, + uint256 maxSupply, + uint256 tokensPerUnit + ) = abi.decode(params, (string, string, uint256, address, bool, uint256, uint256)); + + if (tokensPerUnit == 0) revert InvalidTokensPerUnit(); + + ERC20Mint_BaseToken token = tokenData[slicerId][productId].token; + if (address(token) == address(0)) { + token = new ERC20Mint_BaseToken(name, symbol, maxSupply); + + if (premintAmount != 0) { + token.mint(premintReceiver, premintAmount); + } + } else { + token.setMaxSupply(maxSupply); + } + + tokenData[slicerId][productId] = ERC20Data(token, revertOnMaxSupplyReached, tokensPerUnit); + } + + /** + * @inheritdoc IHookRegistry + */ + function paramsSchema() external pure override returns (string memory) { + return + "string name,string symbol,uint256 premintAmount,address premintReceiver,uint256 maxSupply,uint256 tokensPerUnit"; + } +} diff --git a/src/hooks/actions/ERC20Mint/types/ERC20Data.sol b/src/hooks/actions/ERC20Mint/types/ERC20Data.sol new file mode 100644 index 0000000..0af5a39 --- /dev/null +++ b/src/hooks/actions/ERC20Mint/types/ERC20Data.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ERC20Mint_BaseToken} from "../utils/ERC20Mint_BaseToken.sol"; + +struct ERC20Data { + ERC20Mint_BaseToken token; + bool revertOnMaxSupplyReached; + uint256 tokensPerUnit; +} diff --git a/src/hooks/actions/ERC20Mint/utils/ERC20Mint_BaseToken.sol b/src/hooks/actions/ERC20Mint/utils/ERC20Mint_BaseToken.sol new file mode 100644 index 0000000..15d873c --- /dev/null +++ b/src/hooks/actions/ERC20Mint/utils/ERC20Mint_BaseToken.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ERC20} from "@openzeppelin-4.8.0/token/ERC20/ERC20.sol"; + +/** + * @title ERC20Mint_BaseToken + * @notice Base ERC20 token for ERC20Mint onchain action. + */ +contract ERC20Mint_BaseToken is ERC20 { + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error NotMinter(); + error MaxSupplyExceeded(); + + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + address public immutable minter; + uint256 public maxSupply; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(string memory name_, string memory symbol_, uint256 maxSupply_) ERC20(name_, symbol_) { + minter = msg.sender; + _setMaxSupply(maxSupply_); + } + + /*////////////////////////////////////////////////////////////// + FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function mint(address to, uint256 amount) public { + 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(); + + _setMaxSupply(maxSupply_); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function _setMaxSupply(uint256 maxSupply_) internal { + maxSupply = maxSupply_ == 0 ? type(uint256).max : maxSupply_; + } +} diff --git a/src/hooks/actions/ERC721AMint/ERC721Mint.sol b/src/hooks/actions/ERC721AMint/ERC721Mint.sol new file mode 100644 index 0000000..a606d5b --- /dev/null +++ b/src/hooks/actions/ERC721AMint/ERC721Mint.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IProductsModule, RegistryOnchainAction, HookRegistry, IHookRegistry} from "@/utils/RegistryOnchainAction.sol"; +import {MAX_ROYALTY, ERC721Mint_BaseToken} from "./utils/ERC721Mint_BaseToken.sol"; +import {ERC721Data} from "./types/ERC721Data.sol"; + +/** + * @title ERC721Mint + * @notice Onchain action registry for minting ERC721 tokens on every purchase. + * @dev If `revertOnMaxSupplyReached` is set to true, reverts when max supply is exceeded. + * @author Slice + */ +contract ERC721Mint is RegistryOnchainAction { + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error MaxSupplyExceeded(); + error InvalidRoyaltyFraction(); + + /*////////////////////////////////////////////////////////////// + MUTABLE STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(uint256 slicerId => mapping(uint256 productId => ERC721Data tokenData)) public tokenData; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(IProductsModule productsModuleAddress) RegistryOnchainAction(productsModuleAddress) {} + + /*////////////////////////////////////////////////////////////// + CONFIGURATION + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc RegistryOnchainAction + * @notice Mint tokens to the buyer. + * @dev If `revertOnMaxSupplyReached` is set to true, reverts when max supply is exceeded. + */ + function _onProductPurchase( + uint256 slicerId, + uint256 productId, + address buyer, + uint256 quantity, + bytes memory, + bytes memory + ) internal override { + ERC721Data memory tokenData_ = tokenData[slicerId][productId]; + + (bool success,) = + address(tokenData_.token).call(abi.encodeWithSelector(tokenData_.token.mint.selector, buyer, quantity)); + + if (tokenData_.revertOnMaxSupplyReached) { + if (!success) revert MaxSupplyExceeded(); + } + } + + /** + * @inheritdoc HookRegistry + * @dev Set the ERC721 data for a product. + */ + function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { + ( + string memory name_, + string memory symbol_, + address royaltyReceiver_, + uint256 royaltyFraction_, + string memory baseURI__, + string memory tokenURI__, + bool revertOnMaxSupplyReached, + uint256 maxSupply + ) = abi.decode(params, (string, string, address, uint256, string, string, bool, uint256)); + + if (royaltyFraction_ > MAX_ROYALTY) revert InvalidRoyaltyFraction(); + + ERC721Mint_BaseToken token = tokenData[slicerId][productId].token; + + if (address(token) == address(0)) { + token = new ERC721Mint_BaseToken( + name_, symbol_, maxSupply, royaltyReceiver_, royaltyFraction_, baseURI__, tokenURI__ + ); + } else { + token.setParams(maxSupply, royaltyReceiver_, royaltyFraction_, baseURI__, tokenURI__); + } + + tokenData[slicerId][productId] = ERC721Data(token, revertOnMaxSupplyReached); + } + + /** + * @inheritdoc IHookRegistry + */ + function paramsSchema() external pure override returns (string memory) { + return + "string name,string symbol,address royaltyReceiver,uint256 royaltyFraction,string baseURI,string tokenURI,bool revertOnMaxSupplyReached,uint256 maxSupply"; + } +} diff --git a/src/hooks/actions/ERC721AMint/types/ERC721Data.sol b/src/hooks/actions/ERC721AMint/types/ERC721Data.sol new file mode 100644 index 0000000..2f57fcd --- /dev/null +++ b/src/hooks/actions/ERC721AMint/types/ERC721Data.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ERC721Mint_BaseToken} from "../utils/ERC721Mint_BaseToken.sol"; + +struct ERC721Data { + ERC721Mint_BaseToken token; + bool revertOnMaxSupplyReached; +} diff --git a/src/hooks/actions/ERC721AMint/utils/ERC721Mint_BaseToken.sol b/src/hooks/actions/ERC721AMint/utils/ERC721Mint_BaseToken.sol new file mode 100644 index 0000000..b43f474 --- /dev/null +++ b/src/hooks/actions/ERC721AMint/utils/ERC721Mint_BaseToken.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ERC721A} from "@erc721a/ERC721A.sol"; +import {IERC2981, IERC165} from "@openzeppelin-4.8.0/interfaces/IERC2981.sol"; + +uint256 constant MAX_ROYALTY = 10_000; + +/** + * @title ERC721Mint_BaseToken + * @notice Base ERC721 token for ERC721Mint onchain action. + */ +contract ERC721Mint_BaseToken is ERC721A, IERC2981 { + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error NotMinter(); + error MaxSupplyExceeded(); + + /*////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + address public immutable minter; + + uint256 public maxSupply; + address public royaltyReceiver; + uint256 public royaltyFraction; + string public baseURI_; + string public tokenURI_; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor( + string memory name_, + string memory symbol_, + uint256 maxSupply_, + address royaltyReceiver_, + uint256 royaltyFraction_, + string memory baseURI__, + string memory tokenURI__ + ) ERC721A(name_, symbol_) { + minter = msg.sender; + + _setMaxSupply(maxSupply_); + royaltyReceiver = royaltyReceiver_; + royaltyFraction = royaltyFraction_; + baseURI_ = baseURI__; + tokenURI_ = tokenURI__; + } + + /*////////////////////////////////////////////////////////////// + ACTION FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function mint(address to, uint256 amount) public { + if (msg.sender != minter) revert NotMinter(); + _mint(to, amount); + + if (totalSupply() > maxSupply) revert MaxSupplyExceeded(); + } + + function setParams( + uint256 maxSupply_, + address royaltyReceiver_, + uint256 royaltyFraction_, + string memory baseURI__, + string memory tokenURI__ + ) external { + if (msg.sender != minter) revert NotMinter(); + + royaltyReceiver = royaltyReceiver_; + royaltyFraction = royaltyFraction_; + baseURI_ = baseURI__; + tokenURI_ = tokenURI__; + _setMaxSupply(maxSupply_); + } + + /*////////////////////////////////////////////////////////////// + ERC721A FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc ERC721A + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); + + string memory baseURI = _baseURI(); + return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId))) : tokenURI_; + } + + /** + * @inheritdoc ERC721A + */ + function _baseURI() internal view virtual override returns (string memory) { + return baseURI_; + } + + /** + * @inheritdoc IERC2981 + */ + function royaltyInfo(uint256, uint256 salePrice) + external + view + override + returns (address _receiver, uint256 _royaltyAmount) + { + // return the receiver from storage + _receiver = royaltyReceiver; + + // calculate and return the _royaltyAmount + _royaltyAmount = (salePrice * royaltyFraction) / MAX_ROYALTY; + } + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721A, IERC165) returns (bool) { + // The interface IDs are constants representing the first 4 bytes + // of the XOR of all function selectors in the interface. + // See: [ERC165](https://eips.ethereum.org/EIPS/eip-165) + // (e.g. `bytes4(i.functionA.selector ^ i.functionB.selector ^ ...)`) + return ERC721A.supportsInterface(interfaceId) || interfaceId == type(IERC2981).interfaceId; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function _setMaxSupply(uint256 maxSupply_) internal { + maxSupply = maxSupply_ == 0 ? type(uint256).max : maxSupply_; + } +} diff --git a/src/hooks/actions/NFTGated/NFTGated.sol b/src/hooks/actions/NFTGated/NFTGated.sol new file mode 100644 index 0000000..15d195b --- /dev/null +++ b/src/hooks/actions/NFTGated/NFTGated.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +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 { + IProductsModule, + RegistryOnchainAction, + HookRegistry, + IOnchainAction, + IHookRegistry +} from "@/utils/RegistryOnchainAction.sol"; +import {TokenType, NFTGate, NFTGates} from "./types/NFTGate.sol"; + +/** + * @title NFTGated + * @notice Onchain action registry for NFT gating. + * @author Slice + */ +contract NFTGated is RegistryOnchainAction { + /*////////////////////////////////////////////////////////////// + MUTABLE STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(uint256 slicerId => mapping(uint256 productId => NFTGates gates)) public nftGates; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(IProductsModule productsModuleAddress) RegistryOnchainAction(productsModuleAddress) {} + + /*////////////////////////////////////////////////////////////// + CONFIGURATION + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IOnchainAction + * @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) { + NFTGates memory nftGates_ = nftGates[slicerId][productId]; + + uint256 totalOwned; + unchecked { + 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) { + ++totalOwned; + } + } else if (IERC721(gate.nft).balanceOf(account) >= gate.minQuantity) { + ++totalOwned; + } + + if (totalOwned >= nftGates_.minOwned) return true; + + ++i; + } + } + } + + /** + * @inheritdoc HookRegistry + * @dev Set the NFT gates for a product. + */ + function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { + (NFTGates memory nftGates_) = abi.decode(params, (NFTGates)); + + nftGates[slicerId][productId].minOwned = nftGates_.minOwned; + for (uint256 i = 0; i < nftGates_.gates.length; i++) { + nftGates[slicerId][productId].gates.push(nftGates_.gates[i]); + } + } + + /** + * @inheritdoc IHookRegistry + */ + function paramsSchema() external pure override returns (string memory) { + return "(address nft,uint8 tokenType,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 new file mode 100644 index 0000000..ff2a95c --- /dev/null +++ b/src/hooks/actions/NFTGated/types/NFTGate.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +enum TokenType { + ERC721, + ERC1155 +} + +struct NFTGate { + address nft; + TokenType tokenType; + uint80 id; + uint8 minQuantity; +} + +struct NFTGates { + NFTGate[] gates; + uint256 minOwned; +} diff --git a/src/hooks/actions/README.md b/src/hooks/actions/README.md new file mode 100644 index 0000000..9adb197 --- /dev/null +++ b/src/hooks/actions/README.md @@ -0,0 +1,83 @@ +# 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 + +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 + +## Available Actions + +- **[Allowlisted](./Allowlisted/Allowlisted.sol)**: Onchain action registry for allowlist requirement. +- **[ERC20Gated](./ERC20Gated/ERC20Gated.sol)**: Onchain action registry for ERC20 token gating. +- **[ERC20Mint](./ERC20Mint/ERC20Mint.sol)**: Onchain action registry that mints ERC20 tokens to buyers. +- **[ERC721AMint](./ERC721AMint/ERC721Mint.sol)**: Onchain action registry that mints ERC721A tokens to buyers. +- **[NFTGated](./NFTGated/NFTGated.sol)**: Onchain action registry for NFT gating. + +## Creating Custom Actions + +To create a custom onchain action: + +1. **Inherit from RegistryOnchainAction**: +```solidity +import {RegistryOnchainAction, IProductsModule} from "@/utils/RegistryOnchainAction.sol"; + +contract MyAction is RegistryOnchainAction { + constructor(IProductsModule productsModule) + RegistryOnchainAction(productsModule) {} +} +``` + +2. **Implement required functions**: +```solidity +function isPurchaseAllowed(...) public view override returns (bool) { + // Your eligibility logic here +} + +function _onProductPurchase(...) internal override { + // Custom logic to execute on purchase +} + +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 +} +``` + +## Integration with Slice + +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 diff --git a/src/hooks/pricing/README.md b/src/hooks/pricing/README.md new file mode 100644 index 0000000..2534bba --- /dev/null +++ b/src/hooks/pricing/README.md @@ -0,0 +1,69 @@ +# 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. + +## Key Interface: 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); +} +``` + +## Base Contract: RegistryPricingStrategy + +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 + +## Available Strategies + +- **[TieredDiscount](./TieredDiscount/TieredDiscount.sol)**: Tiered discounts based on asset ownership +- **[LinearVRGDAPrices](./VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol)**: VRGDA with a linear issuance curve - Price library with different params for each Slice product +- **[LogisticVRGDAPrices](./VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol)**: VRGDA with a logistic issuance curve - Price library with different params for each Slice product +- **[VRGDAPrices](./VRGDA/VRGDAPrices.sol)**: Variable Rate Gradual Dutch Auction + +## Creating Custom Pricing Strategies + +To create a custom pricing strategy: + +1. **Inherit from RegistryPricingStrategy**: +```solidity +import {RegistryPricingStrategy, IProductsModule} from "@/utils/RegistryPricingStrategy.sol"; + +contract MyPricingStrategy is RegistryPricingStrategy { + constructor(IProductsModule productsModule) + RegistryPricingStrategy(productsModule) {} +} +``` + +2. **Implement required functions**: +```solidity +function productPrice(...) public view override returns (uint256 ethPrice, uint256 currencyPrice) { + // Your pricing logic here +} + +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 +} +``` + +## Integration with Slice + +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 diff --git a/src/TieredDiscount/NFTDiscount/NFTDiscount.sol b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol similarity index 81% rename from src/TieredDiscount/NFTDiscount/NFTDiscount.sol rename to src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol index 82b24d2..5ac9380 100644 --- a/src/TieredDiscount/NFTDiscount/NFTDiscount.sol +++ b/src/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol @@ -1,46 +1,36 @@ -// SPDX-License-Identifier: Unlicense -pragma solidity ^0.8.19; - -import { - CurrencyParams, - DiscountParams, - ProductDiscounts, - DiscountType, - TieredDiscount, - NFTType -} from "../TieredDiscount.sol"; -import {IERC721} from "@openzeppelin/token/ERC721/IERC721.sol"; -import {IERC1155} from "@openzeppelin/token/ERC1155/IERC1155.sol"; +// SPDX-License-Identifier: MIT +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"; /** - * @title NFTDiscount - Slice pricing strategy with discounts based on NFT ownership - * @author Dom-Mac <@zerohex_eth> - * @author jacopo <@jj_ranalli> + * @title NFTDiscount + * @notice Pricing strategy registry for discounts based on NFT ownership + * @author Slice */ contract NFTDiscount is TieredDiscount { /*////////////////////////////////////////////////////////////// - CONSTRUCTOR + CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(address _productsModuleAddress) TieredDiscount(_productsModuleAddress) {} + constructor(IProductsModule productsModuleAddress) TieredDiscount(productsModuleAddress) {} /*////////////////////////////////////////////////////////////// - FUNCTIONS + CONFIGURATION //////////////////////////////////////////////////////////////*/ /** + * @inheritdoc HookRegistry * @notice Set base price and NFT discounts for a product. * @dev Discounts must be sorted in descending order - * - * @param slicerId ID of the slicer to set the price params for. - * @param productId ID of the product to set the price params for. - * @param allCurrencyParams Array of `CurrencyParams` structs */ - function _setProductPrice(uint256 slicerId, uint256 productId, CurrencyParams[] memory allCurrencyParams) - internal - virtual - override - { + function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { + (CurrencyParams[] memory allCurrencyParams) = abi.decode(params, (CurrencyParams[])); + CurrencyParams memory currencyParams; DiscountParams[] memory newDiscounts; uint256 prevDiscountValue; @@ -112,15 +102,16 @@ contract NFTDiscount is TieredDiscount { } /** - * @notice Function called by Slice protocol to calculate current product price. - * Base price is returned if user does not have a discount. - * - * @param currency Currency chosen for the purchase - * @param quantity Number of units purchased - * @param buyer Address of the buyer - * @param discountParams `ProductDiscounts` struct - * - * @return ethPrice and currencyPrice of product. + * @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"; + } + + /** + * @inheritdoc TieredDiscount + * @notice Base price is returned if user does not have a discount. */ function _productPrice( uint256, @@ -145,7 +136,7 @@ contract NFTDiscount is TieredDiscount { } /*////////////////////////////////////////////////////////////// - INTERNAL + INTERNAL //////////////////////////////////////////////////////////////*/ /** diff --git a/src/hooks/pricing/TieredDiscount/TieredDiscount.sol b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol new file mode 100644 index 0000000..7b20829 --- /dev/null +++ b/src/hooks/pricing/TieredDiscount/TieredDiscount.sol @@ -0,0 +1,86 @@ +// 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"; + +/** + * @title TieredDiscount + * @notice Tiered discounts based on asset ownership + * @author Slice + */ +abstract contract TieredDiscount is RegistryPricingStrategy { + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + error WrongCurrency(); + error InvalidRelativeDiscount(); + error InvalidMinQuantity(); + error DiscountsNotDescending(DiscountParams nft); + + /*////////////////////////////////////////////////////////////// + MUTABLE STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(uint256 slicerId => mapping(uint256 productId => mapping(address currency => ProductDiscounts))) public + productDiscounts; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(IProductsModule productsModuleAddress) RegistryPricingStrategy(productsModuleAddress) {} + + /*////////////////////////////////////////////////////////////// + FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IPricingStrategy + */ + function productPrice( + uint256 slicerId, + uint256 productId, + address currency, + uint256 quantity, + address buyer, + bytes memory data + ) public view override returns (uint256 ethPrice, uint256 currencyPrice) { + ProductDiscounts memory discountParams = productDiscounts[slicerId][productId][currency]; + + if (discountParams.basePrice == 0) { + if (!discountParams.isFree) revert WrongCurrency(); + } else { + return _productPrice(slicerId, productId, currency, quantity, buyer, data, discountParams); + } + } + + /*////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Logic for calculating product price. To be implemented by child contracts. + * + * @param slicerId ID of the slicer to set the price params for. + * @param productId ID of the product to set the price params for. + * @param currency Currency chosen for the purchase + * @param quantity Number of units purchased + * @param buyer Address of the buyer. + * @param data Data passed to the productPrice function. + * @param discountParams `ProductDiscounts` struct. + * + * @return ethPrice and currencyPrice of product. + */ + function _productPrice( + uint256 slicerId, + uint256 productId, + address currency, + uint256 quantity, + address buyer, + bytes memory data, + ProductDiscounts memory discountParams + ) internal view virtual returns (uint256 ethPrice, uint256 currencyPrice); +} diff --git a/src/TieredDiscount/structs/CurrencyParams.sol b/src/hooks/pricing/TieredDiscount/types/CurrencyParams.sol similarity index 96% rename from src/TieredDiscount/structs/CurrencyParams.sol rename to src/hooks/pricing/TieredDiscount/types/CurrencyParams.sol index 9f058d7..afe5d51 100644 --- a/src/TieredDiscount/structs/CurrencyParams.sol +++ b/src/hooks/pricing/TieredDiscount/types/CurrencyParams.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; import {DiscountParams} from "./DiscountParams.sol"; import {DiscountType} from "./ProductDiscounts.sol"; diff --git a/src/TieredDiscount/structs/DiscountParams.sol b/src/hooks/pricing/TieredDiscount/types/DiscountParams.sol similarity index 96% rename from src/TieredDiscount/structs/DiscountParams.sol rename to src/hooks/pricing/TieredDiscount/types/DiscountParams.sol index 4ec4990..26c1a2d 100644 --- a/src/TieredDiscount/structs/DiscountParams.sol +++ b/src/hooks/pricing/TieredDiscount/types/DiscountParams.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; enum NFTType { ERC721, diff --git a/src/TieredDiscount/structs/ProductDiscounts.sol b/src/hooks/pricing/TieredDiscount/types/ProductDiscounts.sol similarity index 95% rename from src/TieredDiscount/structs/ProductDiscounts.sol rename to src/hooks/pricing/TieredDiscount/types/ProductDiscounts.sol index a7ddf86..497a8a2 100644 --- a/src/TieredDiscount/structs/ProductDiscounts.sol +++ b/src/hooks/pricing/TieredDiscount/types/ProductDiscounts.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; import {DiscountParams} from "./DiscountParams.sol"; diff --git a/src/VRGDA/LinearVRGDAPrices.sol b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol similarity index 59% rename from src/VRGDA/LinearVRGDAPrices.sol rename to src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol index 929be4b..cc56197 100644 --- a/src/VRGDA/LinearVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol @@ -1,56 +1,40 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; -import {wadLn, unsafeWadDiv, toDaysWadUnsafe} from "../../utils/SignedWadMath.sol"; -import {IProductsModule} from "../../utils/Slice/interfaces/IProductsModule.sol"; -import {LinearProductParams} from "./structs/LinearProductParams.sol"; -import {LinearVRGDAParams} from "./structs/LinearVRGDAParams.sol"; +import {HookRegistry, IPricingStrategy, IHookRegistry, IProductsModule} from "@/utils/RegistryPricingStrategy.sol"; +import {wadLn, unsafeWadDiv, toDaysWadUnsafe} from "@/utils/math/SignedWadMath.sol"; +import {LinearProductParams} from "../types/LinearProductParams.sol"; +import {LinearVRGDAParams} from "../types/LinearVRGDAParams.sol"; +import {VRGDAPrices} from "../VRGDAPrices.sol"; -import {VRGDAPrices} from "./VRGDAPrices.sol"; - -/// @title Linear Variable Rate Gradual Dutch Auction - Slice pricing strategy -/// @author jacopo -/// @notice VRGDA with a linear issuance curve - Price library with different params for each Slice product. +/// @title LinearVRGDAPrices +/// @notice VRGDA with a linear issuance curve - Price library with different params for each Slice product. +/// @author Slice contract LinearVRGDAPrices is VRGDAPrices { - event ProductPriceSet( - uint256 slicerId, - uint256 productId, - address[] currencies, - LinearVRGDAParams[] linearParams, - int256 priceDecayPercent - ); - /*////////////////////////////////////////////////////////////// - STORAGE + STORAGE //////////////////////////////////////////////////////////////*/ // Mapping from slicerId to productId to ProductParams mapping(uint256 => mapping(uint256 => LinearProductParams)) private _productParams; /*////////////////////////////////////////////////////////////// - CONSTRUCTOR + CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(address productsModuleAddress) VRGDAPrices(productsModuleAddress) {} + constructor(IProductsModule productsModuleAddress) VRGDAPrices(productsModuleAddress) {} /*////////////////////////////////////////////////////////////// - VRGDA PARAMETERS + CONFIGURATION //////////////////////////////////////////////////////////////*/ - /// @notice Set LinearProductParams for product. - /// @param slicerId ID of the slicer to set the price params for. - /// @param productId ID of the product to set the price params for. - /// @param currencies currencies of the product to set the price params for. - /// @param linearParams see `LinearVRGDAParams`. - /// @param priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. - function setProductPrice( - uint256 slicerId, - uint256 productId, - address[] calldata currencies, - LinearVRGDAParams[] calldata linearParams, - int256 priceDecayPercent - ) external onlyProductOwner(slicerId, productId) { - require(linearParams.length == currencies.length, "INVALID_INPUTS"); + /** + * @inheritdoc HookRegistry + * @notice Set LinearVRGDAParams for a product. + */ + function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { + (LinearVRGDAParams[] memory linearParams, int256 priceDecayPercent) = + abi.decode(params, (LinearVRGDAParams[], int256)); int256 decayConstant = wadLn(1e18 - priceDecayPercent); // The decay constant must be negative for VRGDAs to work. @@ -59,8 +43,7 @@ contract LinearVRGDAPrices is VRGDAPrices { /// Get product availability and isInfinite /// @dev available units is a uint32 - (uint256 availableUnits, bool isInfinite) = - IProductsModule(_productsModuleAddress).availableUnits(slicerId, productId); + (uint256 availableUnits, bool isInfinite) = PRODUCTS_MODULE.availableUnits(slicerId, productId); // Product must not have infinite availability require(!isInfinite, "NOT_FINITE_AVAILABILITY"); @@ -71,37 +54,17 @@ contract LinearVRGDAPrices is VRGDAPrices { _productParams[slicerId][productId].decayConstant = int184(decayConstant); // Set currency params - for (uint256 i; i < currencies.length;) { - _productParams[slicerId][productId].pricingParams[currencies[i]] = linearParams[i]; + for (uint256 i; i < linearParams.length;) { + _productParams[slicerId][productId].pricingParams[linearParams[i].currency] = linearParams[i]; unchecked { ++i; } } - - emit ProductPriceSet(slicerId, productId, currencies, linearParams, priceDecayPercent); - } - - /*////////////////////////////////////////////////////////////// - PRICING LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @dev Given a number of products sold, return the target time that number of products should be sold by. - /// @param sold A number of products sold, scaled by 1e18, to get the corresponding target sale time for. - /// @param timeFactor Time-dependent factor used to calculate target sale time. - /// @return The target time the products should be sold by, scaled by 1e18, where the time is - /// relative, such that 0 means the products should be sold immediately when the VRGDA begins. - function getTargetSaleTime(int256 sold, int256 timeFactor) public pure override returns (int256) { - return unsafeWadDiv(sold, timeFactor); } /** - * @notice Function called by Slice protocol to calculate current product price. - * @param slicerId ID of the slicer being queried - * @param productId ID of the product being queried - * @param currency Currency chosen for the purchase - * @param quantity Number of units purchased - * @return ethPrice and currencyPrice of product. + * @inheritdoc IPricingStrategy */ function productPrice( uint256 slicerId, @@ -118,7 +81,7 @@ contract LinearVRGDAPrices is VRGDAPrices { require(productParams.startTime != 0, "PRODUCT_UNSET"); // Get available units - (uint256 availableUnits,) = IProductsModule(_productsModuleAddress).availableUnits(slicerId, productId); + (uint256 availableUnits,) = PRODUCTS_MODULE.availableUnits(slicerId, productId); // Calculate sold units from availableUnits uint256 soldUnits = productParams.startUnits - availableUnits; @@ -146,4 +109,25 @@ contract LinearVRGDAPrices is VRGDAPrices { ); } } + + /** + * @inheritdoc IHookRegistry + */ + function paramsSchema() external pure override returns (string memory) { + return + "(address currency,int128 targetPrice,uint128 min,int256 perTimeUnit)[] linearParams,int256 priceDecayPercent"; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL + //////////////////////////////////////////////////////////////*/ + + /// @dev Given a number of products sold, return the target time that number of products should be sold by. + /// @param sold A number of products sold, scaled by 1e18, to get the corresponding target sale time for. + /// @param timeFactor Time-dependent factor used to calculate target sale time. + /// @return The target time the products should be sold by, scaled by 1e18, where the time is + /// relative, such that 0 means the products should be sold immediately when the VRGDA begins. + function getTargetSaleTime(int256 sold, int256 timeFactor) public pure override returns (int256) { + return unsafeWadDiv(sold, timeFactor); + } } diff --git a/src/VRGDA/LogisticVRGDAPrices.sol b/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol similarity index 73% rename from src/VRGDA/LogisticVRGDAPrices.sol rename to src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol index 5d771cb..52f5aa7 100644 --- a/src/VRGDA/LogisticVRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; +import {HookRegistry, IPricingStrategy, IHookRegistry, IProductsModule} from "@/utils/RegistryPricingStrategy.sol"; import { wadMul, toWadUnsafe, @@ -10,57 +11,39 @@ import { unsafeDiv, wadExp, unsafeWadMul -} from "../../utils/SignedWadMath.sol"; -import {IProductsModule} from "../../utils/Slice/interfaces/IProductsModule.sol"; -import {LogisticProductParams} from "./structs/LogisticProductParams.sol"; -import {LogisticVRGDAParams} from "./structs/LogisticVRGDAParams.sol"; - -import {VRGDAPrices} from "./VRGDAPrices.sol"; - -/// @title Logistic Variable Rate Gradual Dutch Auction - Slice pricing strategy -/// @author jacopo -/// @notice VRGDA with a logistic issuance curve - Price library with different params for each Slice product. +} from "@/utils/math/SignedWadMath.sol"; +import {LogisticProductParams} from "../types/LogisticProductParams.sol"; +import {LogisticVRGDAParams} from "../types/LogisticVRGDAParams.sol"; +import {IProductsModule, VRGDAPrices} from "../VRGDAPrices.sol"; + +/// @title LogisticVRGDAPrices +/// @notice VRGDA with a logistic issuance curve - Price library with different params for each Slice product. +/// @author Slice contract LogisticVRGDAPrices is VRGDAPrices { - event ProductPriceSet( - uint256 slicerId, - uint256 productId, - address[] currencies, - LogisticVRGDAParams[] logisticParams, - int256 priceDecayPercent - ); - /*////////////////////////////////////////////////////////////// - STORAGE + STORAGE //////////////////////////////////////////////////////////////*/ // Mapping from slicerId to productId to LogisticProductParams mapping(uint256 => mapping(uint256 => LogisticProductParams)) private _productParams; /*////////////////////////////////////////////////////////////// - CONSTRUCTOR + CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - constructor(address productsModuleAddress) VRGDAPrices(productsModuleAddress) {} + constructor(IProductsModule productsModuleAddress) VRGDAPrices(productsModuleAddress) {} /*////////////////////////////////////////////////////////////// - VRGDA PARAMETERS + CONFIGURATION //////////////////////////////////////////////////////////////*/ - /// @notice Set LinearProductParams for product. - /// @param slicerId ID of the slicer to set the price params for. - /// @param productId ID of the product to set the price params for. - /// @param currencies currencies of the product to set the price params for. - /// @param logisticParams see `LogisticVRGDAParams`. - /// @param priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. - /// which affects how quickly we will reach the curve's asymptote, scaled by 1e18. - function setProductPrice( - uint256 slicerId, - uint256 productId, - address[] calldata currencies, - LogisticVRGDAParams[] calldata logisticParams, - int256 priceDecayPercent - ) external onlyProductOwner(slicerId, productId) { - require(logisticParams.length == currencies.length, "INVALID_INPUTS"); + /** + * @inheritdoc HookRegistry + * @notice Set LogisticVRGDAParams for a product. + */ + function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { + (LogisticVRGDAParams[] memory logisticParams, int256 priceDecayPercent) = + abi.decode(params, (LogisticVRGDAParams[], int256)); int256 decayConstant = wadLn(1e18 - priceDecayPercent); // The decay constant must be negative for VRGDAs to work. @@ -68,8 +51,7 @@ contract LogisticVRGDAPrices is VRGDAPrices { require(decayConstant >= type(int184).min, "MIN_DECAY_CONSTANT_EXCEEDED"); // Get product availability and isInfinite - (uint256 availableUnits, bool isInfinite) = - IProductsModule(_productsModuleAddress).availableUnits(slicerId, productId); + (uint256 availableUnits, bool isInfinite) = PRODUCTS_MODULE.availableUnits(slicerId, productId); // Product must not have infinite availability require(!isInfinite, "NON_FINITE_AVAILABILITY"); @@ -80,19 +62,71 @@ contract LogisticVRGDAPrices is VRGDAPrices { _productParams[slicerId][productId].decayConstant = int184(decayConstant); // Set currency params - for (uint256 i; i < currencies.length;) { - _productParams[slicerId][productId].pricingParams[currencies[i]] = logisticParams[i]; + for (uint256 i; i < logisticParams.length;) { + _productParams[slicerId][productId].pricingParams[logisticParams[i].currency] = logisticParams[i]; unchecked { ++i; } } + } + + /** + * @inheritdoc IPricingStrategy + */ + function productPrice( + uint256 slicerId, + uint256 productId, + address currency, + uint256 quantity, + address, + bytes memory + ) public view override returns (uint256 ethPrice, uint256 currencyPrice) { + // Add reference for product and pricing params + LogisticProductParams storage productParams = _productParams[slicerId][productId]; + LogisticVRGDAParams memory pricingParams = productParams.pricingParams[currency]; + + require(productParams.startTime != 0, "PRODUCT_UNSET"); + + // Get available units + (uint256 availableUnits,) = PRODUCTS_MODULE.availableUnits(slicerId, productId); + + // Set ethPrice or currencyPrice based on chosen currency + if (currency == address(0)) { + ethPrice = getAdjustedVRGDALogisticPrice( + pricingParams.targetPrice, + productParams.decayConstant, + toDaysWadUnsafe(block.timestamp - productParams.startTime), + toWadUnsafe(productParams.startUnits + 1), + productParams.startUnits - availableUnits, + pricingParams.timeScale, + pricingParams.min, + quantity + ); + } else { + currencyPrice = getAdjustedVRGDALogisticPrice( + pricingParams.targetPrice, + productParams.decayConstant, + toDaysWadUnsafe(block.timestamp - productParams.startTime), + toWadUnsafe(productParams.startUnits + 1), + productParams.startUnits - availableUnits, + pricingParams.timeScale, + pricingParams.min, + quantity + ); + } + } - emit ProductPriceSet(slicerId, productId, currencies, logisticParams, priceDecayPercent); + /** + * @inheritdoc IHookRegistry + */ + function paramsSchema() external pure override returns (string memory) { + return + "(address currency,int128 targetPrice,uint128 min,int256 timeScale)[] logisticParams,int256 priceDecayPercent"; } /*////////////////////////////////////////////////////////////// - PRICING LOGIC + PRICING LOGIC //////////////////////////////////////////////////////////////*/ /// @notice Same as `getVRGDAPrice` but which additionally accepts `logisticLimit` and @@ -188,55 +222,4 @@ contract LogisticVRGDAPrices is VRGDAPrices { return -unsafeWadDiv(wadLn(saleFactor - 1e18), timeFactor); } } - - /** - * @notice Function called by Slice protocol to calculate current product price. - * @param slicerId ID of the slicer being queried - * @param productId ID of the product being queried - * @param currency Currency chosen for the purchase - * @param quantity Number of units purchased - * @return ethPrice and currencyPrice of product. - */ - function productPrice( - uint256 slicerId, - uint256 productId, - address currency, - uint256 quantity, - address, - bytes memory - ) public view override returns (uint256 ethPrice, uint256 currencyPrice) { - // Add reference for product and pricing params - LogisticProductParams storage productParams = _productParams[slicerId][productId]; - LogisticVRGDAParams memory pricingParams = productParams.pricingParams[currency]; - - require(productParams.startTime != 0, "PRODUCT_UNSET"); - - // Get available units - (uint256 availableUnits,) = IProductsModule(_productsModuleAddress).availableUnits(slicerId, productId); - - // Set ethPrice or currencyPrice based on chosen currency - if (currency == address(0)) { - ethPrice = getAdjustedVRGDALogisticPrice( - pricingParams.targetPrice, - productParams.decayConstant, - toDaysWadUnsafe(block.timestamp - productParams.startTime), - toWadUnsafe(productParams.startUnits + 1), - productParams.startUnits - availableUnits, - pricingParams.timeScale, - pricingParams.min, - quantity - ); - } else { - currencyPrice = getAdjustedVRGDALogisticPrice( - pricingParams.targetPrice, - productParams.decayConstant, - toDaysWadUnsafe(block.timestamp - productParams.startTime), - toWadUnsafe(productParams.startUnits + 1), - productParams.startUnits - availableUnits, - pricingParams.timeScale, - pricingParams.min, - quantity - ); - } - } } diff --git a/src/VRGDA/VRGDAPrices.sol b/src/hooks/pricing/VRGDA/VRGDAPrices.sol similarity index 61% rename from src/VRGDA/VRGDAPrices.sol rename to src/hooks/pricing/VRGDA/VRGDAPrices.sol index f07335c..27cb5da 100644 --- a/src/VRGDA/VRGDAPrices.sol +++ b/src/hooks/pricing/VRGDA/VRGDAPrices.sol @@ -1,44 +1,23 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; -import {wadExp, wadMul, unsafeWadMul, toWadUnsafe} from "../../utils/SignedWadMath.sol"; -import {ISliceProductPrice} from "../../utils/Slice/interfaces/utils/ISliceProductPrice.sol"; -import {IProductsModule} from "../../utils/Slice/interfaces/IProductsModule.sol"; - -/// @title Variable Rate Gradual Dutch Auction - Slice pricing strategy -/// @author jacopo -/// @notice Price library with configurable params for each Slice product. - -abstract contract VRGDAPrices is ISliceProductPrice { - /*////////////////////////////////////////////////////////////// - STORAGE - //////////////////////////////////////////////////////////////*/ - - address internal immutable _productsModuleAddress; - - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - constructor(address productsModuleAddress) { - _productsModuleAddress = productsModuleAddress; - } +import {wadExp, wadMul, unsafeWadMul, toWadUnsafe} from "@/utils/math/SignedWadMath.sol"; +import {IProductsModule, RegistryPricingStrategy} from "@/utils/RegistryPricingStrategy.sol"; +/** + * @title VRGDAPrices + * @notice Variable Rate Gradual Dutch Auction + * @author Slice + */ +abstract contract VRGDAPrices is RegistryPricingStrategy { /*////////////////////////////////////////////////////////////// - MODIFIERS + CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - /// @notice Check if msg.sender is owner of a product. Used to manage access of `setProductPrice` - /// in implementations of this contract. - modifier onlyProductOwner(uint256 slicerId, uint256 productId) { - require( - IProductsModule(_productsModuleAddress).isProductOwner(slicerId, productId, msg.sender), "NOT_PRODUCT_OWNER" - ); - _; - } + constructor(IProductsModule productsModuleAddress) RegistryPricingStrategy(productsModuleAddress) {} /*////////////////////////////////////////////////////////////// - PRICING LOGIC + PRICING LOGIC //////////////////////////////////////////////////////////////*/ /// @notice Calculate the price of a product according to the VRGDA formula. @@ -110,23 +89,4 @@ abstract contract VRGDAPrices is ISliceProductPrice { } } } - - /** - * @notice Function called by Slice protocol to calculate current product price. - * @param slicerId ID of the slicer being queried - * @param productId ID of the product being queried - * @param currency Currency chosen for the purchase - * @param quantity Number of units purchased - * @param buyer Address of the buyer - * @param data Custom data sent along with the purchase transaction by the buyer - * @return ethPrice and currencyPrice of product. - */ - function productPrice( - uint256 slicerId, - uint256 productId, - address currency, - uint256 quantity, - address buyer, - bytes memory data - ) public view virtual override returns (uint256 ethPrice, uint256 currencyPrice) {} } diff --git a/src/VRGDA/structs/LinearProductParams.sol b/src/hooks/pricing/VRGDA/types/LinearProductParams.sol similarity index 95% rename from src/VRGDA/structs/LinearProductParams.sol rename to src/hooks/pricing/VRGDA/types/LinearProductParams.sol index b77c04e..a15829b 100644 --- a/src/VRGDA/structs/LinearProductParams.sol +++ b/src/hooks/pricing/VRGDA/types/LinearProductParams.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; import {LinearVRGDAParams} from "./LinearVRGDAParams.sol"; diff --git a/src/VRGDA/structs/LinearVRGDAParams.sol b/src/hooks/pricing/VRGDA/types/LinearVRGDAParams.sol similarity index 88% rename from src/VRGDA/structs/LinearVRGDAParams.sol rename to src/hooks/pricing/VRGDA/types/LinearVRGDAParams.sol index 7f272f4..c3f591f 100644 --- a/src/VRGDA/structs/LinearVRGDAParams.sol +++ b/src/hooks/pricing/VRGDA/types/LinearVRGDAParams.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; /// @param targetPrice Target price for a product, to be scaled according to sales pace. /// @param min minimum price to be paid for a token, scaled by 1e18 /// @param perTimeUnit The total number of products to target selling every full unit of time. struct LinearVRGDAParams { + address currency; int128 targetPrice; uint128 min; int256 perTimeUnit; diff --git a/src/VRGDA/structs/LogisticProductParams.sol b/src/hooks/pricing/VRGDA/types/LogisticProductParams.sol similarity index 95% rename from src/VRGDA/structs/LogisticProductParams.sol rename to src/hooks/pricing/VRGDA/types/LogisticProductParams.sol index e016755..0b1181a 100644 --- a/src/VRGDA/structs/LogisticProductParams.sol +++ b/src/hooks/pricing/VRGDA/types/LogisticProductParams.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; import {LogisticVRGDAParams} from "./LogisticVRGDAParams.sol"; diff --git a/src/VRGDA/structs/LogisticVRGDAParams.sol b/src/hooks/pricing/VRGDA/types/LogisticVRGDAParams.sol similarity index 90% rename from src/VRGDA/structs/LogisticVRGDAParams.sol rename to src/hooks/pricing/VRGDA/types/LogisticVRGDAParams.sol index 5adf031..31a3576 100644 --- a/src/VRGDA/structs/LogisticVRGDAParams.sol +++ b/src/hooks/pricing/VRGDA/types/LogisticVRGDAParams.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; /// @param targetPrice Target price for a product, to be scaled according to sales pace. /// @param min minimum price to be paid for a token, scaled by 1e18 /// @param timeScale Time scale controls the steepness of the logistic curve, /// which affects how quickly we will reach the curve's asymptote. struct LogisticVRGDAParams { + address currency; int128 targetPrice; uint128 min; int256 timeScale; diff --git a/src/hooks/pricingActions/FirstForFree/FirstForFree.sol b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol new file mode 100644 index 0000000..8de82ed --- /dev/null +++ b/src/hooks/pricingActions/FirstForFree/FirstForFree.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC721} from "@openzeppelin-4.8.0/interfaces/IERC721.sol"; +import {IERC1155} from "@openzeppelin-4.8.0/interfaces/IERC1155.sol"; +import {IOnchainAction} from "@/interfaces/IOnchainAction.sol"; +import { + IPricingStrategy, + RegistryOnchainAction, + RegistryPricingStrategyAction, + HookRegistry, + IHookRegistry, + IProductsModule +} from "@/utils/RegistryPricingStrategyAction.sol"; +import {ProductParams, TokenCondition} from "./types/ProductParams.sol"; +import {TokenType} from "./types/TokenCondition.sol"; +import {ITokenERC1155} from "./utils/ITokenERC1155.sol"; + +/** + * @title FirstForFree + * @notice Discounts the first purchase of a product for free, based on conditions. + * @author Slice + */ +contract FirstForFree is RegistryPricingStrategyAction { + /*////////////////////////////////////////////////////////////// + MUTABLE STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(uint256 slicerId => mapping(uint256 productId => ProductParams price)) public usdcPrices; + mapping(address buyer => mapping(uint256 slicerId => uint256 purchases)) public purchases; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(IProductsModule productsModuleAddress) RegistryPricingStrategyAction(productsModuleAddress) {} + + /*////////////////////////////////////////////////////////////// + CONFIGURATION + //////////////////////////////////////////////////////////////*/ + + /** + * @inheritdoc IPricingStrategy + * @notice Applies discount only for first N purchases on a slicer. + */ + function productPrice(uint256 slicerId, uint256 productId, address, uint256 quantity, address buyer, bytes memory) + public + view + override + returns (uint256 ethPrice, uint256 currencyPrice) + { + ProductParams memory productParams = usdcPrices[slicerId][productId]; + + if (_isEligible(buyer, productParams.eligibleTokens)) { + uint256 totalPurchases = purchases[buyer][slicerId]; + if (totalPurchases < productParams.freeUnits) { + unchecked { + uint256 freeUnitsLeft = productParams.freeUnits - totalPurchases; + if (quantity <= freeUnitsLeft) { + return (0, 0); + } else { + return (0, usdcPrices[slicerId][productId].usdcPrice * (quantity - freeUnitsLeft)); + } + } + } + } + + return (0, usdcPrices[slicerId][productId].usdcPrice * quantity); + } + + /** + * @inheritdoc RegistryOnchainAction + * @notice Mint `quantity` NFTs to `account` on purchase. Keeps track of total purchases. + */ + function _onProductPurchase( + uint256 slicerId, + uint256 productId, + address buyer, + uint256 quantity, + bytes memory, + bytes memory + ) internal override { + purchases[buyer][slicerId] += quantity; + + ProductParams memory productParams = usdcPrices[slicerId][productId]; + if (productParams.mintToken != address(0)) { + ITokenERC1155(productParams.mintToken).mintTo(buyer, productParams.mintTokenId, "", quantity); + } + } + + /** + * @inheritdoc HookRegistry + * @notice Sets the product parameters. + */ + function _configureProduct(uint256 slicerId, uint256 productId, bytes memory params) internal override { + ( + uint256 usdcPrice, + TokenCondition[] memory eligibleTokens, + address mintToken, + uint88 mintTokenId, + uint8 freeUnits + ) = abi.decode(params, (uint256, TokenCondition[], address, uint88, uint8)); + + ProductParams storage productParams = usdcPrices[slicerId][productId]; + + productParams.usdcPrice = usdcPrice; + productParams.mintToken = mintToken; + productParams.mintTokenId = mintTokenId; + productParams.freeUnits = freeUnits; + + // Remove all discount tokens + delete productParams.eligibleTokens; + + for (uint256 i = 0; i < eligibleTokens.length;) { + productParams.eligibleTokens.push(eligibleTokens[i]); + + unchecked { + ++i; + } + } + } + + /** + * @inheritdoc IHookRegistry + */ + function paramsSchema() external pure override returns (string memory) { + return + "uint256 usdcPrice,(address tokenAddress,uint8 tokenType,uint88 tokenId,uint8 minQuantity)[] eligibleTokens,address mintToken,uint88 mintTokenId,uint8 freeUnits"; + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function _isEligible(address buyer, TokenCondition[] memory eligibleTokens) + internal + view + returns (bool isEligible) + { + isEligible = eligibleTokens.length == 0; + if (!isEligible) { + TokenCondition memory tokenCondition; + for (uint256 i = 0; i < eligibleTokens.length;) { + tokenCondition = eligibleTokens[i]; + + isEligible = tokenCondition.tokenType == TokenType.ERC721 + ? IERC721(tokenCondition.tokenAddress).balanceOf(buyer) >= tokenCondition.minQuantity + : IERC1155(tokenCondition.tokenAddress).balanceOf(buyer, tokenCondition.tokenId) + >= tokenCondition.minQuantity; + + if (isEligible) break; + + unchecked { + ++i; + } + } + } + } +} diff --git a/src/hooks/pricingActions/FirstForFree/types/ProductParams.sol b/src/hooks/pricingActions/FirstForFree/types/ProductParams.sol new file mode 100644 index 0000000..a77e158 --- /dev/null +++ b/src/hooks/pricingActions/FirstForFree/types/ProductParams.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {TokenCondition} from "./TokenCondition.sol"; + +struct ProductParams { + uint256 usdcPrice; + TokenCondition[] eligibleTokens; + address mintToken; + uint88 mintTokenId; + uint8 freeUnits; +} diff --git a/src/hooks/pricingActions/FirstForFree/types/TokenCondition.sol b/src/hooks/pricingActions/FirstForFree/types/TokenCondition.sol new file mode 100644 index 0000000..044e5c5 --- /dev/null +++ b/src/hooks/pricingActions/FirstForFree/types/TokenCondition.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +struct TokenCondition { + address tokenAddress; + TokenType tokenType; + uint80 tokenId; + uint8 minQuantity; +} + +enum TokenType { + ERC721, + ERC1155 +} diff --git a/src/hooks/pricingActions/FirstForFree/utils/ITokenERC1155.sol b/src/hooks/pricingActions/FirstForFree/utils/ITokenERC1155.sol new file mode 100644 index 0000000..4664ec3 --- /dev/null +++ b/src/hooks/pricingActions/FirstForFree/utils/ITokenERC1155.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface ITokenERC1155 { + /** + * @notice Lets an account with MINTER_ROLE mint an NFT. + * + * @param to The address to mint the NFT to. + * @param tokenId The tokenId of the NFTs to mint + * @param uri The URI to assign to the NFT. + * @param amount The number of copies of the NFT to mint. + * + */ + function mintTo(address to, uint256 tokenId, string calldata uri, uint256 amount) external; +} diff --git a/src/hooks/pricingActions/README.md b/src/hooks/pricingActions/README.md new file mode 100644 index 0000000..3a1165d --- /dev/null +++ b/src/hooks/pricingActions/README.md @@ -0,0 +1,98 @@ +# Pricing Strategy Actions + +Pricing strategy actions combine both pricing strategies and onchain actions in a single contract. They implement both `IPricingStrategy` and `IOnchainAction` interfaces, allowing them to calculate dynamic prices AND execute custom logic during purchases. + +## Key Interfaces + +**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 + +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 + +## Available Pricing Strategy Actions + +- **[FirstForFree](./FirstForFree/FirstForFree.sol)**: Discounts the first purchase of a product for free, based on conditions. + +## Creating Custom Pricing Strategy Actions + +To create a custom pricing strategy action: + +1. **Inherit from RegistryPricingStrategyAction**: +```solidity +import {RegistryPricingStrategyAction, IProductsModule} from "@/utils/RegistryPricingStrategyAction.sol"; + +contract MyPricingStrategyAction is RegistryPricingStrategyAction { + constructor(IProductsModule productsModule) + RegistryPricingStrategyAction(productsModule) {} +} +``` + +2. **Implement required functions**: +```solidity +function productPrice(...) public view override returns (uint256 ethPrice, uint256 currencyPrice) { + // Your pricing logic here +} + +function isPurchaseAllowed(...) public view override returns (bool) { + // Your eligibility logic here +} + +function _onProductPurchase(...) internal override { + // Custom logic to execute on purchase +} + +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 +} +``` + +## Integration with Slice + +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 diff --git a/src/interfaces/IHookRegistry.sol b/src/interfaces/IHookRegistry.sol new file mode 100644 index 0000000..d55495a --- /dev/null +++ b/src/interfaces/IHookRegistry.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "slice/interfaces/hooks/IHookRegistry.sol"; diff --git a/src/interfaces/IOnchainAction.sol b/src/interfaces/IOnchainAction.sol new file mode 100644 index 0000000..15fcc3b --- /dev/null +++ b/src/interfaces/IOnchainAction.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "slice/interfaces/hooks/IOnchainAction.sol"; diff --git a/src/interfaces/IPricingStrategy.sol b/src/interfaces/IPricingStrategy.sol new file mode 100644 index 0000000..94f2594 --- /dev/null +++ b/src/interfaces/IPricingStrategy.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "slice/interfaces/hooks/IPricingStrategy.sol"; diff --git a/src/utils/OnchainAction.sol b/src/utils/OnchainAction.sol new file mode 100644 index 0000000..7843e87 --- /dev/null +++ b/src/utils/OnchainAction.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "slice/utils/hooks/OnchainAction.sol"; diff --git a/src/utils/PricingStrategy.sol b/src/utils/PricingStrategy.sol new file mode 100644 index 0000000..66f2d12 --- /dev/null +++ b/src/utils/PricingStrategy.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "slice/utils/hooks/PricingStrategy.sol"; diff --git a/src/utils/PricingStrategyAction.sol b/src/utils/PricingStrategyAction.sol new file mode 100644 index 0000000..cebf111 --- /dev/null +++ b/src/utils/PricingStrategyAction.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "slice/utils/hooks/PricingStrategyAction.sol"; diff --git a/src/utils/RegistryOnchainAction.sol b/src/utils/RegistryOnchainAction.sol new file mode 100644 index 0000000..679480d --- /dev/null +++ b/src/utils/RegistryOnchainAction.sol @@ -0,0 +1,4 @@ +// 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 new file mode 100644 index 0000000..bb1609e --- /dev/null +++ b/src/utils/RegistryPricingStrategy.sol @@ -0,0 +1,4 @@ +// 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 new file mode 100644 index 0000000..09acaf8 --- /dev/null +++ b/src/utils/RegistryPricingStrategyAction.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "slice/utils/hooks/RegistryPricingStrategyAction.sol"; diff --git a/utils/SignedWadMath.sol b/src/utils/math/SignedWadMath.sol similarity index 99% rename from utils/SignedWadMath.sol rename to src/utils/math/SignedWadMath.sol index bfe5244..14dfc8e 100644 --- a/utils/SignedWadMath.sol +++ b/src/utils/math/SignedWadMath.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; /// @title Signed Wad Math /// @author transmissions11 diff --git a/test/mocks/MockLinearVRGDAPrices.sol b/test/mocks/MockLinearVRGDAPrices.sol deleted file mode 100644 index 6418b07..0000000 --- a/test/mocks/MockLinearVRGDAPrices.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "../../src/VRGDA/LinearVRGDAPrices.sol"; - -contract MockLinearVRGDAPrices is LinearVRGDAPrices { - constructor(address productsModuleAddress) LinearVRGDAPrices(productsModuleAddress) {} -} diff --git a/test/mocks/MockLogisticVRGDAPrices.sol b/test/mocks/MockLogisticVRGDAPrices.sol deleted file mode 100644 index 8614dce..0000000 --- a/test/mocks/MockLogisticVRGDAPrices.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "../../src/VRGDA/LogisticVRGDAPrices.sol"; - -contract MockLogisticVRGDAPrices is LogisticVRGDAPrices { - constructor(address productsModuleAddress) LogisticVRGDAPrices(productsModuleAddress) {} -} diff --git a/test/mocks/MockProductsModule.sol b/test/mocks/MockProductsModule.sol deleted file mode 100644 index 4f6e876..0000000 --- a/test/mocks/MockProductsModule.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -contract MockProductsModule { - function isProductOwner(uint256, uint256, address account) external pure returns (bool isAllowed) { - isAllowed = account == address(0); - } - - function availableUnits(uint256, uint256) external pure returns (uint256 units, bool isInfinite) { - units = 6392; - isInfinite = false; - } -} diff --git a/test/ERC721Discount.sol b/test/pricingStrategies/NFTDiscount/NFTDiscount.sol similarity index 93% rename from test/ERC721Discount.sol rename to test/pricingStrategies/NFTDiscount/NFTDiscount.sol index 8f9ea34..3aff126 100644 --- a/test/ERC721Discount.sol +++ b/test/pricingStrategies/NFTDiscount/NFTDiscount.sol @@ -1,17 +1,17 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +pragma solidity ^0.8.20; -import {Test} from "forge-std/Test.sol"; +import {RegistryPricingStrategyTest} from "@test/utils/RegistryPricingStrategyTest.sol"; import {console2} from "forge-std/console2.sol"; -import {MockProductsModule} from "./mocks/MockProductsModule.sol"; import { + IProductsModule, NFTDiscount, ProductDiscounts, DiscountType, DiscountParams, CurrencyParams, NFTType -} from "../src/TieredDiscount/NFTDiscount/NFTDiscount.sol"; +} from "@/hooks/pricing/TieredDiscount/NFTDiscount/NFTDiscount.sol"; import {MockERC721} from "./mocks/MockERC721.sol"; import {MockERC1155} from "./mocks/MockERC1155.sol"; @@ -25,9 +25,8 @@ uint80 constant fixedDiscountOne = 100; uint80 constant fixedDiscountTwo = 200; uint80 constant percentDiscount = 1000; // 10% -contract NFTDiscountTest is Test { +contract NFTDiscountTest is RegistryPricingStrategyTest { NFTDiscount erc721GatedDiscount; - MockProductsModule productsModule; MockERC721 nftOne = new MockERC721(); MockERC721 nftTwo = new MockERC721(); MockERC721 nftThree = new MockERC721(); @@ -38,8 +37,8 @@ contract NFTDiscountTest is Test { uint8 minNftQuantity = 1; function setUp() public { - productsModule = new MockProductsModule(); - erc721GatedDiscount = new NFTDiscount(address(productsModule)); + erc721GatedDiscount = new NFTDiscount(PRODUCTS_MODULE); + _setHook(address(erc721GatedDiscount)); nftOne.mint(buyer); } @@ -55,7 +54,7 @@ contract NFTDiscountTest is Test { currenciesParams[0] = CurrencyParams(ETH, basePrice, false, DiscountType.Absolute, discounts); vm.prank(owner); - erc721GatedDiscount.setProductPrice(slicerId, productId, currenciesParams); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(currenciesParams)); } function testDeploy() public view { @@ -78,7 +77,7 @@ contract NFTDiscountTest is Test { currenciesParams[0] = CurrencyParams(ETH, basePrice, false, DiscountType.Absolute, discounts); vm.prank(owner); - erc721GatedDiscount.setProductPrice(slicerId, productId, currenciesParams); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(currenciesParams)); /// check product price (uint256 ethPrice, uint256 currencyPrice) = @@ -104,7 +103,7 @@ contract NFTDiscountTest is Test { currenciesParams[0] = CurrencyParams(USDC, basePrice, false, DiscountType.Absolute, discounts); vm.prank(owner); - erc721GatedDiscount.setProductPrice(slicerId, productId, currenciesParams); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(currenciesParams)); /// check product price (uint256 ethPrice, uint256 currencyPrice) = @@ -130,7 +129,7 @@ contract NFTDiscountTest is Test { currenciesParams[0] = CurrencyParams(USDC, basePrice, false, DiscountType.Absolute, discounts); vm.prank(owner); - erc721GatedDiscount.setProductPrice(slicerId, productId, currenciesParams); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(currenciesParams)); /// check product price (uint256 ethPrice, uint256 currencyPrice) = @@ -175,7 +174,7 @@ contract NFTDiscountTest is Test { currenciesParams[1] = CurrencyParams(USDC, basePrice, false, DiscountType.Absolute, discountsTwo); vm.prank(owner); - erc721GatedDiscount.setProductPrice(slicerId, productId, currenciesParams); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(currenciesParams)); /// check product price for ETH (uint256 ethPrice, uint256 currencyPrice) = @@ -301,7 +300,7 @@ contract NFTDiscountTest is Test { currenciesParams[0] = CurrencyParams(ETH, basePrice, false, DiscountType.Relative, discounts); vm.prank(owner); - erc721GatedDiscount.setProductPrice(slicerId, productId, currenciesParams); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(currenciesParams)); /// check product price (uint256 ethPrice, uint256 currencyPrice) = @@ -327,7 +326,7 @@ contract NFTDiscountTest is Test { currenciesParams[0] = CurrencyParams(ETH, basePrice, false, DiscountType.Relative, discounts); vm.prank(owner); - erc721GatedDiscount.setProductPrice(slicerId, productId, currenciesParams); + erc721GatedDiscount.configureProduct(slicerId, productId, abi.encode(currenciesParams)); // buy multiple products quantity = 6; diff --git a/test/mocks/MockERC1155.sol b/test/pricingStrategies/NFTDiscount/mocks/MockERC1155.sol similarity index 71% rename from test/mocks/MockERC1155.sol rename to test/pricingStrategies/NFTDiscount/mocks/MockERC1155.sol index 67f893f..eeb4383 100644 --- a/test/mocks/MockERC1155.sol +++ b/test/pricingStrategies/NFTDiscount/mocks/MockERC1155.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; -import {ERC1155} from "@openzeppelin/token/ERC1155/ERC1155.sol"; +import {ERC1155} from "@openzeppelin-4.8.0/token/ERC1155/ERC1155.sol"; contract MockERC1155 is ERC1155 { uint256 public constant tokenId = 1; diff --git a/test/mocks/MockERC721.sol b/test/pricingStrategies/NFTDiscount/mocks/MockERC721.sol similarity index 70% rename from test/mocks/MockERC721.sol rename to test/pricingStrategies/NFTDiscount/mocks/MockERC721.sol index 6665b4b..c3b4cc4 100644 --- a/test/mocks/MockERC721.sol +++ b/test/pricingStrategies/NFTDiscount/mocks/MockERC721.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; -import {ERC721} from "@openzeppelin/token/ERC721/ERC721.sol"; +import {ERC721} from "@openzeppelin-4.8.0/token/ERC721/ERC721.sol"; contract MockERC721 is ERC721 { uint256 public tokenId; diff --git a/test/LinearVRGDA.t.sol b/test/pricingStrategies/VRGDA/LinearVRGDA.t.sol similarity index 84% rename from test/LinearVRGDA.t.sol rename to test/pricingStrategies/VRGDA/LinearVRGDA.t.sol index 5baa92b..1409aa9 100644 --- a/test/LinearVRGDA.t.sol +++ b/test/pricingStrategies/VRGDA/LinearVRGDA.t.sol @@ -1,13 +1,11 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +pragma solidity ^0.8.20; -import {Test} from "forge-std/Test.sol"; -import {console2} from "forge-std/console2.sol"; - -import {wadLn, toWadUnsafe, toDaysWadUnsafe, fromDaysWadUnsafe} from "../utils/SignedWadMath.sol"; +import {RegistryPricingStrategyTest} from "@test/utils/RegistryPricingStrategyTest.sol"; +import {wadLn, toWadUnsafe, toDaysWadUnsafe, fromDaysWadUnsafe} from "@/utils/math/SignedWadMath.sol"; import "./mocks/MockLinearVRGDAPrices.sol"; -import {MockProductsModule} from "./mocks/MockProductsModule.sol"; +import {IProductsModule} from "@/utils/PricingStrategy.sol"; uint256 constant ONE_THOUSAND_YEARS = 356 days * 1000; @@ -20,24 +18,21 @@ uint128 constant min = 1e18; int256 constant priceDecayPercent = 0.31e18; int256 constant perTimeUnit = 2e18; -contract LinearVRGDATest is Test { +contract LinearVRGDATest is RegistryPricingStrategyTest { MockLinearVRGDAPrices vrgda; - MockProductsModule productsModule; function setUp() public { - productsModule = new MockProductsModule(); - vrgda = new MockLinearVRGDAPrices(address(productsModule)); + vrgda = new MockLinearVRGDAPrices(PRODUCTS_MODULE); + _setHook(address(vrgda)); + + LinearVRGDAParams[] memory linearParams = new LinearVRGDAParams[](2); + linearParams[0] = LinearVRGDAParams(address(0), targetPriceConstant, min, perTimeUnit); + linearParams[1] = LinearVRGDAParams(address(20), targetPriceConstant, min, perTimeUnit); - LinearVRGDAParams[] memory linearParams = new LinearVRGDAParams[](1); - linearParams[0] = LinearVRGDAParams(targetPriceConstant, min, perTimeUnit); - address[] memory ethCurrency = new address[](1); - ethCurrency[0] = address(0); - address[] memory erc20Currency = new address[](1); - erc20Currency[0] = address(20); + bytes memory params = abi.encode(linearParams, priceDecayPercent); vm.startPrank(address(0)); - vrgda.setProductPrice(slicerId, productId, ethCurrency, linearParams, priceDecayPercent); - vrgda.setProductPrice(slicerId, productId, erc20Currency, linearParams, priceDecayPercent); + vrgda.configureProduct(slicerId, productId, params); vm.stopPrank(); } @@ -127,14 +122,13 @@ contract LinearVRGDATest is Test { function testSetMultiplePrices() public { uint256 productId_ = 2; LinearVRGDAParams[] memory linearParams = new LinearVRGDAParams[](2); - linearParams[0] = LinearVRGDAParams(targetPriceConstant, min, perTimeUnit); - linearParams[1] = LinearVRGDAParams(targetPriceConstant, min, perTimeUnit); - address[] memory currencies = new address[](2); - currencies[0] = address(0); - currencies[1] = address(20); + linearParams[0] = LinearVRGDAParams(address(0), targetPriceConstant, min, perTimeUnit); + linearParams[1] = LinearVRGDAParams(address(20), targetPriceConstant, min, perTimeUnit); + + bytes memory params = abi.encode(linearParams, priceDecayPercent); vm.startPrank(address(0)); - vrgda.setProductPrice(slicerId, productId_, currencies, linearParams, priceDecayPercent); + vrgda.configureProduct(slicerId, productId_, params); vm.stopPrank(); // Our VRGDA targets this number of mints at given time. diff --git a/test/LogisticVRGDA.t.sol b/test/pricingStrategies/VRGDA/LogisticVRGDA.t.sol similarity index 89% rename from test/LogisticVRGDA.t.sol rename to test/pricingStrategies/VRGDA/LogisticVRGDA.t.sol index 01c70ab..c040522 100644 --- a/test/LogisticVRGDA.t.sol +++ b/test/pricingStrategies/VRGDA/LogisticVRGDA.t.sol @@ -1,11 +1,10 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +pragma solidity ^0.8.20; -import {Test} from "forge-std/Test.sol"; -import {unsafeDiv, wadLn, toWadUnsafe, toDaysWadUnsafe, fromDaysWadUnsafe} from "../utils/SignedWadMath.sol"; +import {RegistryPricingStrategyTest} from "@test/utils/RegistryPricingStrategyTest.sol"; +import {unsafeDiv, wadLn, toWadUnsafe, toDaysWadUnsafe, fromDaysWadUnsafe} from "@/utils/math/SignedWadMath.sol"; import "./mocks/MockLogisticVRGDAPrices.sol"; -import {MockProductsModule} from "./mocks/MockProductsModule.sol"; import "forge-std/console2.sol"; uint256 constant ONE_THOUSAND_YEARS = 356 days * 1000; @@ -21,24 +20,21 @@ int256 constant timeScale = 0.0023e18; int256 constant logisticLimitAdjusted = int256((MAX_SELLABLE + 1) * 2e18); int256 constant logisticLimitDoubled = int256((MAX_SELLABLE + 1e18) * 2e18); -contract LogisticVRGDATest is Test { +contract LogisticVRGDATest is RegistryPricingStrategyTest { MockLogisticVRGDAPrices vrgda; - MockProductsModule productsModule; function setUp() public { - productsModule = new MockProductsModule(); - vrgda = new MockLogisticVRGDAPrices(address(productsModule)); + vrgda = new MockLogisticVRGDAPrices(PRODUCTS_MODULE); + _setHook(address(vrgda)); - LogisticVRGDAParams[] memory logisticParams = new LogisticVRGDAParams[](1); - logisticParams[0] = LogisticVRGDAParams(targetPriceConstant, min, timeScale); - address[] memory ethCurrency = new address[](1); - ethCurrency[0] = address(0); - address[] memory erc20Currency = new address[](1); - erc20Currency[0] = address(20); + LogisticVRGDAParams[] memory logisticParams = new LogisticVRGDAParams[](2); + logisticParams[0] = LogisticVRGDAParams(address(0), targetPriceConstant, min, timeScale); + logisticParams[1] = LogisticVRGDAParams(address(20), targetPriceConstant, min, timeScale); + + bytes memory params = abi.encode(logisticParams, priceDecayPercent); vm.startPrank(address(0)); - vrgda.setProductPrice(slicerId, productId, ethCurrency, logisticParams, priceDecayPercent); - vrgda.setProductPrice(slicerId, productId, erc20Currency, logisticParams, priceDecayPercent); + vrgda.configureProduct(slicerId, productId, params); vm.stopPrank(); } @@ -166,14 +162,13 @@ contract LogisticVRGDATest is Test { // uint256 targetPriceTest = 7.3013e18; uint256 productIdTest = 2; LogisticVRGDAParams[] memory logisticParams = new LogisticVRGDAParams[](2); - logisticParams[0] = LogisticVRGDAParams(targetPriceConstant, min, timeScale); - logisticParams[1] = LogisticVRGDAParams(targetPriceConstant, min, timeScale); - address[] memory currencies = new address[](2); - currencies[0] = address(0); - currencies[1] = address(20); + logisticParams[0] = LogisticVRGDAParams(address(0), targetPriceConstant, min, timeScale); + logisticParams[1] = LogisticVRGDAParams(address(20), targetPriceConstant, min, timeScale); + + bytes memory params = abi.encode(logisticParams, priceDecayPercent); vm.startPrank(address(0)); - vrgda.setProductPrice(slicerId, productIdTest, currencies, logisticParams, priceDecayPercent); + vrgda.configureProduct(slicerId, productIdTest, params); vm.stopPrank(); vm.warp(block.timestamp + 10 days); diff --git a/test/correctness/LinearVRGDACorrectness.t.sol b/test/pricingStrategies/VRGDA/correctness/LinearVRGDACorrectness.t.sol similarity index 83% rename from test/correctness/LinearVRGDACorrectness.t.sol rename to test/pricingStrategies/VRGDA/correctness/LinearVRGDACorrectness.t.sol index d399bb5..110f367 100644 --- a/test/correctness/LinearVRGDACorrectness.t.sol +++ b/test/pricingStrategies/VRGDA/correctness/LinearVRGDACorrectness.t.sol @@ -1,17 +1,14 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import {Test} from "forge-std/Test.sol"; - -import {wadLn, toWadUnsafe} from "../../utils/SignedWadMath.sol"; - -import "../mocks/MockLinearVRGDAPrices.sol"; -import {MockProductsModule} from "../mocks/MockProductsModule.sol"; +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 {wadLn, toWadUnsafe} from "@/utils/math/SignedWadMath.sol"; +import {IProductsModule} from "@/utils/PricingStrategy.sol"; +import {MockLinearVRGDAPrices, LinearVRGDAParams} from "../mocks/MockLinearVRGDAPrices.sol"; -contract LinearVRGDACorrectnessTest is Test { +contract LinearVRGDACorrectnessTest is RegistryPricingStrategyTest { // Sample parameters for differential fuzzing campaign. uint256 constant maxTimeframe = 356 days * 10; uint256 constant maxSellable = 10000; @@ -24,19 +21,17 @@ contract LinearVRGDACorrectnessTest is Test { int256 constant perTimeUnit = 2e18; MockLinearVRGDAPrices vrgda; - MockProductsModule productsModule; function setUp() public { - productsModule = new MockProductsModule(); - vrgda = new MockLinearVRGDAPrices(address(productsModule)); + vrgda = new MockLinearVRGDAPrices(PRODUCTS_MODULE); + _setHook(address(vrgda)); LinearVRGDAParams[] memory linearParams = new LinearVRGDAParams[](1); - linearParams[0] = LinearVRGDAParams(targetPriceConstant, min, perTimeUnit); - address[] memory ethCurrency = new address[](1); - ethCurrency[0] = address(0); + linearParams[0] = LinearVRGDAParams(address(0), targetPriceConstant, min, perTimeUnit); vm.prank(address(0)); - vrgda.setProductPrice(slicerId, productId, ethCurrency, linearParams, priceDecayPercent); + bytes memory params = abi.encode(linearParams, priceDecayPercent); + vrgda.configureProduct(slicerId, productId, params); } function testFFICorrectness() public { diff --git a/test/correctness/python/VRGDA.py b/test/pricingStrategies/VRGDA/correctness/python/VRGDA.py similarity index 100% rename from test/correctness/python/VRGDA.py rename to test/pricingStrategies/VRGDA/correctness/python/VRGDA.py diff --git a/test/correctness/python/compute_price.py b/test/pricingStrategies/VRGDA/correctness/python/compute_price.py similarity index 100% rename from test/correctness/python/compute_price.py rename to test/pricingStrategies/VRGDA/correctness/python/compute_price.py diff --git a/test/correctness/python/requirements.txt b/test/pricingStrategies/VRGDA/correctness/python/requirements.txt similarity index 100% rename from test/correctness/python/requirements.txt rename to test/pricingStrategies/VRGDA/correctness/python/requirements.txt diff --git a/test/pricingStrategies/VRGDA/mocks/MockLinearVRGDAPrices.sol b/test/pricingStrategies/VRGDA/mocks/MockLinearVRGDAPrices.sol new file mode 100644 index 0000000..61226a7 --- /dev/null +++ b/test/pricingStrategies/VRGDA/mocks/MockLinearVRGDAPrices.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@/hooks/pricing/VRGDA/LinearVRGDAPrices/LinearVRGDAPrices.sol"; + +contract MockLinearVRGDAPrices is LinearVRGDAPrices { + constructor(IProductsModule productsModule) LinearVRGDAPrices(productsModule) {} +} diff --git a/test/pricingStrategies/VRGDA/mocks/MockLogisticVRGDAPrices.sol b/test/pricingStrategies/VRGDA/mocks/MockLogisticVRGDAPrices.sol new file mode 100644 index 0000000..0f7d0f7 --- /dev/null +++ b/test/pricingStrategies/VRGDA/mocks/MockLogisticVRGDAPrices.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@/hooks/pricing/VRGDA/LogisticVRGDAPrices/LogisticVRGDAPrices.sol"; + +contract MockLogisticVRGDAPrices is LogisticVRGDAPrices { + constructor(IProductsModule productsModule) LogisticVRGDAPrices(productsModule) {} +} diff --git a/test/utils/HookRegistryTest.sol b/test/utils/HookRegistryTest.sol new file mode 100644 index 0000000..d0a9f58 --- /dev/null +++ b/test/utils/HookRegistryTest.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {HookTest} from "./HookTest.sol"; +import {IHookRegistry} from "@/utils/RegistryPricingStrategy.sol"; + +abstract contract HookRegistryTest is HookTest { + // TODO: + function testParamsSchema() public view { + string memory schema = IHookRegistry(hook).paramsSchema(); + assertTrue(bytes(schema).length > 0); + } +} diff --git a/test/utils/HookTest.sol b/test/utils/HookTest.sol new file mode 100644 index 0000000..e9b25e5 --- /dev/null +++ b/test/utils/HookTest.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {IProductsModule} from "@/utils/OnchainAction.sol"; + +abstract contract HookTest is Test { + MockProductsModule public mockProductsModule = new MockProductsModule(); + IProductsModule public PRODUCTS_MODULE = IProductsModule(address(mockProductsModule)); + + address public hook; + + function _setHook(address _hookAddress) internal { + hook = _hookAddress; + } + + function testSetup_HookInitialized() public view { + assertTrue(hook != address(0), "Hook address is not set with `_setHook`"); + } + + // TODO: parse the schema and generate the params + // function generateParamsFromSchema(string memory schema) public pure returns (bytes memory) { + // string[] memory params = vm.split(schema, ","); + + // example schema: "(address currency,int128 targetPrice,uint128 min,int256 perTimeUnit)[] linearParams,int256 priceDecayPercent"; + + // for (uint256 i = 0; i < params.length; i++) { + // string[] memory keyValue = vm.split(params[i], " "); + // ... + // } + // } +} + +contract MockProductsModule { + function isProductOwner(uint256, uint256, address account) external pure returns (bool isAllowed) { + isAllowed = account == address(0); + } + + function availableUnits(uint256, uint256) external pure returns (uint256 units, bool isInfinite) { + units = 6392; + isInfinite = false; + } +} diff --git a/test/utils/OnchainActionTest.sol b/test/utils/OnchainActionTest.sol new file mode 100644 index 0000000..f6acea7 --- /dev/null +++ b/test/utils/OnchainActionTest.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {HookTest} from "./HookTest.sol"; +import {IOnchainAction} from "@/utils/OnchainAction.sol"; + +abstract contract OnchainActionTest is HookTest { + function testSupportsInterface_OnchainAction() public view { + assertTrue(IOnchainAction(hook).supportsInterface(type(IOnchainAction).interfaceId)); + } +} diff --git a/test/utils/PricingStrategyActionTest.sol b/test/utils/PricingStrategyActionTest.sol new file mode 100644 index 0000000..bc93872 --- /dev/null +++ b/test/utils/PricingStrategyActionTest.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {HookTest} from "./HookTest.sol"; +import {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)); + } +} diff --git a/test/utils/PricingStrategyTest.sol b/test/utils/PricingStrategyTest.sol new file mode 100644 index 0000000..ef08096 --- /dev/null +++ b/test/utils/PricingStrategyTest.sol @@ -0,0 +1,11 @@ +// 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/RegistryOnchainActionTest.sol b/test/utils/RegistryOnchainActionTest.sol new file mode 100644 index 0000000..36911eb --- /dev/null +++ b/test/utils/RegistryOnchainActionTest.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {HookRegistryTest} from "./HookRegistryTest.sol"; +import {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)); + } +} diff --git a/test/utils/RegistryPricingStrategyActionTest.sol b/test/utils/RegistryPricingStrategyActionTest.sol new file mode 100644 index 0000000..6f46da3 --- /dev/null +++ b/test/utils/RegistryPricingStrategyActionTest.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {HookRegistryTest} from "./HookRegistryTest.sol"; +import {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)); + } +} diff --git a/test/utils/RegistryPricingStrategyTest.sol b/test/utils/RegistryPricingStrategyTest.sol new file mode 100644 index 0000000..8b83327 --- /dev/null +++ b/test/utils/RegistryPricingStrategyTest.sol @@ -0,0 +1,12 @@ +// 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/utils/Slice/interfaces/IFundsModule.sol b/utils/Slice/interfaces/IFundsModule.sol deleted file mode 100644 index 4988f20..0000000 --- a/utils/Slice/interfaces/IFundsModule.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -import "./ISlicer.sol"; -import "./ISliceCore.sol"; - -interface IFundsModule { - function JBProjectId() external view returns (uint256 projectId); - - function sliceCore() external view returns (ISliceCore sliceCoreAddress); - - function balances(address account, address currency) - external - view - returns (uint128 accountBalance, uint128 protocolPayment); - - function depositEth(address account, uint256 protocolPayment) external payable; - - function depositTokenFromSlicer( - uint256 tokenId, - address account, - address currency, - uint256 amount, - uint256 protocolPayment - ) external; - - function withdraw(address account, address currency) external; - - function batchWithdraw(address account, address[] memory currencies) external; - - function withdrawOnRelease( - uint256 tokenId, - address account, - address currency, - uint256 amount, - uint256 protocolPayment - ) external payable; - - function batchReleaseSlicers(ISlicer[] memory slicers, address account, address currency, bool triggerWithdraw) - external; -} diff --git a/utils/Slice/interfaces/IProductsModule.sol b/utils/Slice/interfaces/IProductsModule.sol deleted file mode 100644 index 6ad2aca..0000000 --- a/utils/Slice/interfaces/IProductsModule.sol +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -import "../structs/Function.sol"; -import "../structs/Price.sol"; -import "../structs/ProductParams.sol"; -import "../structs/PurchaseParams.sol"; -import "./ISliceCore.sol"; -import "./IFundsModule.sol"; - -interface IProductsModule { - function sliceCore() external view returns (ISliceCore sliceCoreAddress); - - function fundsModule() external view returns (IFundsModule fundsModuleAddress); - - function addProduct(uint256 slicerId, ProductParams memory params, Function memory externalCall_) external; - - function setProductInfo( - uint256 slicerId, - uint256 productId, - uint8 newMaxUnits, - bool isFree, - bool isInfinite, - uint32 newUnits, - CurrencyPrice[] memory currencyPrices - ) external; - - function removeProduct(uint256 slicerId, uint256 productId) external; - - function payProducts(address buyer, PurchaseParams[] calldata purchases) external payable; - - function releaseEthToSlicer(uint256 slicerId) external; - - // function _setCategoryAddress(uint256 categoryIndex, address newCategoryAddress) external; - - function ethBalance(uint256 slicerId) external view returns (uint256); - - function productPrice( - uint256 slicerId, - uint256 productId, - address currency, - uint256 quantity, - address buyer, - bytes memory data - ) external view returns (Price memory price); - - function validatePurchaseUnits(address account, uint256 slicerId, uint256 productId) - external - view - returns (uint256 purchases); - - function validatePurchase(uint256 slicerId, uint256 productId) - external - view - returns (uint256 purchases, bytes memory purchaseData); - - function availableUnits(uint256 slicerId, uint256 productId) - external - view - returns (uint256 units, bool isInfinite); - - function isProductOwner(uint256 slicerId, uint256 productId, address account) - external - view - returns (bool isAllowed); - - function nextProductId(uint256 slicerId) external view returns (uint256); - - // function categoryAddress(uint256 categoryIndex) external view returns (address); -} diff --git a/utils/Slice/interfaces/ISliceCore.sol b/utils/Slice/interfaces/ISliceCore.sol deleted file mode 100644 index fcc0049..0000000 --- a/utils/Slice/interfaces/ISliceCore.sol +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -import "./ISlicerManager.sol"; -import "../structs/SliceParams.sol"; -import "../structs/SlicerParams.sol"; -import "@openzeppelin/interfaces/IERC1155.sol"; -import "@openzeppelin/interfaces/IERC2981.sol"; - -interface ISliceCore is IERC1155, IERC2981 { - function slicerManager() external view returns (ISlicerManager slicerManagerAddress); - - function slice(SliceParams calldata params) external; - - function reslice(uint256 tokenId, address payable[] calldata accounts, int32[] calldata tokensDiffs) external; - - function slicerBatchTransfer( - address from, - address[] memory recipients, - uint256 id, - uint256[] memory amounts, - bool release - ) external; - - function safeTransferFromUnreleased(address from, address to, uint256 id, uint256 amount, bytes memory data) - external; - - function setController(uint256 id, address newController) external; - - function setRoyalty(uint256 tokenId, bool isSlicer, bool isActive, uint256 royaltyPercentage) external; - - function slicers(uint256 id) external view returns (address); - - function controller(uint256 id) external view returns (address); - - function totalSupply(uint256 id) external view returns (uint256); - - function supply() external view returns (uint256); - - function exists(uint256 id) external view returns (bool); - - function owner() external view returns (address owner); - - function _setBasePath(string calldata basePath_) external; - - function _togglePause() external; -} diff --git a/utils/Slice/interfaces/ISlicer.sol b/utils/Slice/interfaces/ISlicer.sol deleted file mode 100644 index 176993f..0000000 --- a/utils/Slice/interfaces/ISlicer.sol +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -import "@openzeppelin/token/ERC1155/IERC1155Receiver.sol"; -import "@openzeppelin/token/ERC721/IERC721Receiver.sol"; - -interface ISlicer is IERC721Receiver, IERC1155Receiver { - function release(address account, address currency, bool withdraw) external; - - function batchReleaseAccounts(address[] memory accounts, address currency, bool withdraw) external; - - function unreleased(address account, address currency) external view returns (uint256 unreleasedAmount); - - function getFee() external view returns (uint256 fee); - - function getFeeForAccount(address account) external view returns (uint256 fee); - - function slicerInfo() - external - view - returns ( - uint256 tokenId, - uint256 minimumShares, - address creator, - bool isImmutable, - bool currenciesControlled, - bool productsControlled, - bool acceptsAllCurrencies, - address[] memory currencies - ); - - function isPayeeAllowed(address payee) external view returns (bool); - - function acceptsCurrency(address currency) external view returns (bool); - - function _updatePayees( - address payable sender, - address receiver, - bool toRelease, - uint256 senderShares, - uint256 transferredShares - ) external; - - function _updatePayeesReslice(address payable[] memory accounts, int32[] memory tokensDiffs, uint32 totalSupply) - external; - - function _setChildSlicer(uint256 id, bool addChildSlicerMode) external; - - function _setTotalShares(uint256 totalShares) external; - - function _addCurrencies(address[] memory currencies) external; - - function _setCustomFee(bool customFeeActive, uint256 customFee) external; - - function _releaseFromSliceCore(address account, address currency, uint256 accountSlices) external; - - function _releaseFromFundsModule(address account, address currency) - external - returns (uint256 amount, uint256 protocolPayment); - - function _handle721Purchase(address buyer, address contractAddress, uint256 tokenId) external; - - function _handle1155Purchase(address buyer, address contractAddress, uint256 quantity, uint256 tokenId) external; -} diff --git a/utils/Slice/interfaces/ISlicerManager.sol b/utils/Slice/interfaces/ISlicerManager.sol deleted file mode 100644 index 7f6a513..0000000 --- a/utils/Slice/interfaces/ISlicerManager.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -import "../structs/SliceParams.sol"; - -interface ISlicerManager { - function implementation() external view returns (address); - - function _createSlicer(address creator, uint256 id, SliceParams calldata params) external returns (address); - - function _upgradeSlicers(address newLogicImpl) external; -} diff --git a/utils/Slice/interfaces/utils/IPriceFeed.sol b/utils/Slice/interfaces/utils/IPriceFeed.sol deleted file mode 100644 index a5fdc18..0000000 --- a/utils/Slice/interfaces/utils/IPriceFeed.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "../../structs/PoolData.sol"; - -interface IPriceFeed { - function uniswapV3Factory() external view returns (address uniswapV3Factory); - - function pools(address token0, address token1) - external - view - returns (address poolAddress, uint24 fee, uint48 lastUpdatedTimestamp); - - function getPool(address tokenA, address tokenB) external view returns (PoolData memory pool); - - function getQuote(uint128 baseAmount, address baseToken, address quoteToken, uint32 secondsAgo) - external - view - returns (uint256 quoteAmount); - - function getUpdatedPool(address tokenA, address tokenB, uint256 updateInterval) - external - returns (PoolData memory pool); - - function getQuoteAndUpdatePool( - uint128 baseAmount, - address baseToken, - address quoteToken, - uint32 secondsAgo, - uint256 updateInterval - ) external returns (uint256 quoteAmount); - - function updatePool(address tokenA, address tokenB) external returns (PoolData memory highestLiquidityPool); -} diff --git a/utils/Slice/interfaces/utils/ISliceProductPrice.sol b/utils/Slice/interfaces/utils/ISliceProductPrice.sol deleted file mode 100644 index 6ac9b8c..0000000 --- a/utils/Slice/interfaces/utils/ISliceProductPrice.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -interface ISliceProductPrice { - function productPrice( - uint256 slicerId, - uint256 productId, - address currency, - uint256 quantity, - address buyer, - bytes memory data - ) external view returns (uint256 ethPrice, uint256 currencyPrice); -} diff --git a/utils/Slice/structs/Balance.sol b/utils/Slice/structs/Balance.sol deleted file mode 100644 index 12bd64e..0000000 --- a/utils/Slice/structs/Balance.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -struct Balance { - uint128 account; - uint128 protocol; -} diff --git a/utils/Slice/structs/Category.sol b/utils/Slice/structs/Category.sol deleted file mode 100644 index e8b42c3..0000000 --- a/utils/Slice/structs/Category.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -struct Category { - address categoryAddress; -} diff --git a/utils/Slice/structs/CurrencyPrice.sol b/utils/Slice/structs/CurrencyPrice.sol deleted file mode 100644 index 24cf06c..0000000 --- a/utils/Slice/structs/CurrencyPrice.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -struct CurrencyPrice { - uint248 value; - bool dynamicPricing; - address externalAddress; - address currency; -} diff --git a/utils/Slice/structs/Function.sol b/utils/Slice/structs/Function.sol deleted file mode 100644 index 14e4d5d..0000000 --- a/utils/Slice/structs/Function.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -/** - * @param data data sent to `externalAddress` - * @param value Amount or percentage of ETH / token forwarded to `externalAddress` - * @param externalAddress Address to be called during external call - * @param checkFunctionSignature The timestamp when the slicer becomes releasable - * @param execFunctionSignature The timestamp when the slicer becomes transferable - */ -struct Function { - bytes data; - uint256 value; - address externalAddress; - bytes4 checkFunctionSignature; - bytes4 execFunctionSignature; -} diff --git a/utils/Slice/structs/Payee.sol b/utils/Slice/structs/Payee.sol deleted file mode 100644 index 21db3d6..0000000 --- a/utils/Slice/structs/Payee.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -struct Payee { - address account; - uint32 shares; - bool transfersAllowedWhileLocked; -} diff --git a/utils/Slice/structs/PoolData.sol b/utils/Slice/structs/PoolData.sol deleted file mode 100644 index d3a10b4..0000000 --- a/utils/Slice/structs/PoolData.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -struct PoolData { - address poolAddress; - uint24 fee; - uint48 lastUpdatedTimestamp; -} diff --git a/utils/Slice/structs/Price.sol b/utils/Slice/structs/Price.sol deleted file mode 100644 index d7fcd78..0000000 --- a/utils/Slice/structs/Price.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -struct Price { - uint256 eth; - uint256 currency; - uint256 ethExternalCall; - uint256 currencyExternalCall; -} diff --git a/utils/Slice/structs/Product.sol b/utils/Slice/structs/Product.sol deleted file mode 100644 index ab733c7..0000000 --- a/utils/Slice/structs/Product.sol +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "./Function.sol"; -import "./SubSlicerProduct.sol"; -import "./ProductPrice.sol"; -import "./Purchases.sol"; - -/** - * @notice Struct related to product info. - * - * @param purchases Mapping of quantity bought by addresses - * @param prices Mapping with prices set for the product for each allowed currency - * @param subSlicerProducts Mapping with Array of subProducts - * @param externalCall `Function` struct containing the params to execute an external call during purchase - * @param data Metadata containing the public information about the product - * @param purchaseData Metadata containing the purchase information/procedure for the buyers - * @param creator Address of the account who created the product - * @param priceEditTimestamp Timestamp of last time the product price was edited - * @param availableUnits Number of available units on sale - * @param maxUnitsPerBuyer Maximum amount of units allowed to purchase for a buyer - * @param packedBooleans boolean flags ordered from the right: [IsFree, IsInfinite] - */ -struct Product { - mapping(address => Purchases) purchases; - mapping(address => ProductPrice) prices; - SubSlicerProduct[] subSlicerProducts; - Function externalCall; - bytes data; - bytes purchaseData; - address creator; - uint40 priceEditTimestamp; - uint32 availableUnits; - uint8 maxUnitsPerBuyer; - uint8 packedBooleans; -} -// uint32 categoryIndex; diff --git a/utils/Slice/structs/ProductParams.sol b/utils/Slice/structs/ProductParams.sol deleted file mode 100644 index cad8ec8..0000000 --- a/utils/Slice/structs/ProductParams.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "./SubSlicerProduct.sol"; -import "./CurrencyPrice.sol"; - -struct ProductParams { - SubSlicerProduct[] subSlicerProducts; - CurrencyPrice[] currencyPrices; - bytes data; - bytes purchaseData; - uint32 availableUnits; - // uint32 categoryIndex; - uint8 maxUnitsPerBuyer; - bool isFree; - bool isInfinite; - bool isExternalCallPaymentRelative; - bool isExternalCallPreferredToken; -} diff --git a/utils/Slice/structs/ProductPrice.sol b/utils/Slice/structs/ProductPrice.sol deleted file mode 100644 index bc7d734..0000000 --- a/utils/Slice/structs/ProductPrice.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -struct ProductPrice { - uint248 value; - bool dynamicPricing; - address externalAddress; -} diff --git a/utils/Slice/structs/PurchaseParams.sol b/utils/Slice/structs/PurchaseParams.sol deleted file mode 100644 index 9a149ce..0000000 --- a/utils/Slice/structs/PurchaseParams.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -struct PurchaseParams { - uint128 slicerId; - uint32 quantity; - address currency; - uint32 productId; - bytes buyerCustomData; -} diff --git a/utils/Slice/structs/Purchases.sol b/utils/Slice/structs/Purchases.sol deleted file mode 100644 index 4b245c8..0000000 --- a/utils/Slice/structs/Purchases.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -struct Purchases { - uint32 quantity; -} diff --git a/utils/Slice/structs/SliceParams.sol b/utils/Slice/structs/SliceParams.sol deleted file mode 100644 index 71338a7..0000000 --- a/utils/Slice/structs/SliceParams.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "../structs/Payee.sol"; - -/** - * @param payees Addresses and shares of the initial payees - * @param minimumShares Amount of shares that gives an account access to restricted - * @param currencies Array of tokens accepted by the slicer - * @param releaseTimelock The timestamp when the slicer becomes releasable - * @param transferTimelock The timestamp when the slicer becomes transferable - * @param controller The address of the slicer controller - * @param slicerFlags See `_flags` in {Slicer} - * @param sliceCoreFlags See `flags` in {SlicerParams} struct - */ -struct SliceParams { - Payee[] payees; - uint256 minimumShares; - address[] currencies; - uint256 releaseTimelock; - uint40 transferTimelock; - address controller; - uint8 slicerFlags; - uint8 sliceCoreFlags; -} diff --git a/utils/Slice/structs/SlicerParams.sol b/utils/Slice/structs/SlicerParams.sol deleted file mode 100644 index 592295b..0000000 --- a/utils/Slice/structs/SlicerParams.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "../interfaces/ISlicer.sol"; - -/** - * @param slicer ISlicer instance - * @param controller Address of slicer controller - * @param transferTimelock The timestamp when the slicer becomes transferable - * @param totalSupply Total supply of slices - * @param royaltyPercentage Percentage of royalty to claim (up to 100, ie 10%) - * @param flags Boolean flags in the following order from the right: [isCustomRoyaltyActive, isRoyaltyReceiverSlicer, - * isResliceAllowed, isControlledTransferAllowed] - * @param transferAllowlist Mapping from address to permission to transfer slices during transferTimelock period - */ -struct SlicerParams { - ISlicer slicer; - address controller; - uint40 transferTimelock; - uint32 totalSupply; - uint8 royaltyPercentage; - uint8 flags; - uint8 FREE_SLOT_1; - mapping(address => bool) transferAllowlist; -} diff --git a/utils/Slice/structs/SubSlicerProduct.sol b/utils/Slice/structs/SubSlicerProduct.sol deleted file mode 100644 index eaa791c..0000000 --- a/utils/Slice/structs/SubSlicerProduct.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -struct SubSlicerProduct { - uint128 subSlicerId; - uint32 subProductId; -}