diff --git a/foundry.toml b/foundry.toml index 84496f73..20ad511d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,7 @@ solc = '0.8.23' vm_version = 'paris' # Required for L2s (Optimism, Arbitrum, etc.) sizes = true verbosity = 3 # display errors -optimizer_runs = 1000 +optimizer_runs = 800 block_number = 14126430 block_timestamp = 1643802347 runs = 4096 diff --git a/package-lock.json b/package-lock.json index 575183fc..1a965021 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,9 @@ "license": "MIT", "dependencies": { "@bananapus/address-registry": "^0.0.4", - "@bananapus/core": "^0.0.25", + "@bananapus/core": "^0.0.30", "@bananapus/ownable": "^0.0.6", - "@bananapus/permission-ids": "^0.0.11", + "@bananapus/permission-ids": "^0.0.12", "@openzeppelin/contracts": "^5.0.2", "@prb/math": "^4.0.3" }, @@ -243,11 +243,12 @@ "integrity": "sha512-3VHv3IcgQqzvzijZXuQCSjQP30SiXb6njfdYPBzKRljNVWp0vQUUahicNpzUjuyzymAdQcgSH8xQBZedf3kT/w==" }, "node_modules/@bananapus/core": { - "version": "0.0.25", - "resolved": "https://registry.npmjs.org/@bananapus/core/-/core-0.0.25.tgz", - "integrity": "sha512-0bY58vMIxqLvr9Q3rir486ERE5/tZ1jwmqGX4YYIzJ8q3kIGDsg7nFx9z5P3leyu7ml4ROyBrrHtzprRsIV57w==", + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@bananapus/core/-/core-0.0.30.tgz", + "integrity": "sha512-sWBcJwagfVIATeyybHNjdH+T/k6+/8vJpy3uz7zF7XTOG6VXpYmN9c0NvrGXKE53xTeWDCDxaZKz4mKrX7r+UQ==", + "license": "MIT", "dependencies": { - "@bananapus/permission-ids": "^0.0.11", + "@bananapus/permission-ids": "^0.0.12", "@chainlink/contracts": "^1.1.1", "@openzeppelin/contracts": "^5.0.2", "@prb/math": "^4.0.3", @@ -287,9 +288,9 @@ "integrity": "sha512-eC1Pkon47hth4z4dRDOACTJICsLLZf/9KH4W+9VUXQMcT3+4UbzNZ3kw2gJ3WdkuFZgIQLjNZt1oQ68RP29KRg==" }, "node_modules/@bananapus/permission-ids": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/@bananapus/permission-ids/-/permission-ids-0.0.11.tgz", - "integrity": "sha512-Vw2DYCkyHiq97rcRyg50teibvN5c3qeP/habrbKNJYEbYnR7RDb0efeFf1rWuLLlzpHGzDyPYHEZcP1lh6z22Q==", + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@bananapus/permission-ids/-/permission-ids-0.0.12.tgz", + "integrity": "sha512-nEgiJoC0DJy030RDAb5pLM8xQC1BVnNoXE1xKVZ/YB/+EwCy8260SHRxyzafF5On1aAD4raL8WFjr3/K/Lbanw==", "engines": { "node": ">=20.0.0" } diff --git a/package.json b/package.json index 59f46a5e..857ebcb9 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,9 @@ }, "dependencies": { "@bananapus/address-registry": "^0.0.4", - "@bananapus/core": "^0.0.25", + "@bananapus/core": "^0.0.30", "@bananapus/ownable": "^0.0.6", - "@bananapus/permission-ids": "^0.0.11", + "@bananapus/permission-ids": "^0.0.12", "@openzeppelin/contracts": "^5.0.2", "@prb/math": "^4.0.3" }, diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index ea5c271d..c2d8f3ab 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -60,31 +60,33 @@ contract DeployScript is Script, Sphinx { : JBAddressRegistry(_registry); } + JB721TiersHookStore store; + { + // Perform the check for the store. + (address _store, bool _storeIsDeployed) = + _isDeployed(HOOK_STORE_SALT, type(JB721TiersHookStore).creationCode, ""); + + // Deploy it if it has not been deployed yet. + store = !_storeIsDeployed ? new JB721TiersHookStore{salt: HOOK_STORE_SALT}() : JB721TiersHookStore(_store); + } + JB721TiersHook hook; { // Perform the check for the registry. (address _hook, bool _hookIsDeployed) = _isDeployed( HOOK_SALT, type(JB721TiersHook).creationCode, - abi.encode(core.directory, core.permissions, TRUSTED_FORWARDER) + abi.encode(core.directory, core.permissions, core.rulesets, store, TRUSTED_FORWARDER) ); // Deploy it if it has not been deployed yet. hook = !_hookIsDeployed - ? new JB721TiersHook{salt: HOOK_SALT}(core.directory, core.permissions, TRUSTED_FORWARDER) + ? new JB721TiersHook{salt: HOOK_SALT}( + core.directory, core.permissions, core.rulesets, store, TRUSTED_FORWARDER + ) : JB721TiersHook(_hook); } - JB721TiersHookStore store; - { - // Perform the check for the store. - (address _store, bool _storeIsDeployed) = - _isDeployed(HOOK_STORE_SALT, type(JB721TiersHookStore).creationCode, ""); - - // Deploy it if it has not been deployed yet. - store = !_storeIsDeployed ? new JB721TiersHookStore{salt: HOOK_STORE_SALT}() : JB721TiersHookStore(_store); - } - JB721TiersHookDeployer hookDeployer; { // Perform the check for the registry. diff --git a/src/JB721TiersHook.sol b/src/JB721TiersHook.sol index 245a51d1..8b60b25f 100644 --- a/src/JB721TiersHook.sol +++ b/src/JB721TiersHook.sol @@ -1,32 +1,32 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {Context} from "@openzeppelin/contracts/utils/Context.sol"; -import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol"; -import {mulDiv} from "@prb/math/src/Common.sol"; -import {JBOwnable} from "@bananapus/ownable/src/JBOwnable.sol"; -import {JBOwnableOverrides} from "@bananapus/ownable/src/JBOwnableOverrides.sol"; +import {IJBDirectory} from "@bananapus/core/src/interfaces/IJBDirectory.sol"; import {IJBPermissions} from "@bananapus/core/src/interfaces/IJBPermissions.sol"; -import {IJBRulesets} from "@bananapus/core/src/interfaces/IJBRulesets.sol"; import {IJBPrices} from "@bananapus/core/src/interfaces/IJBPrices.sol"; -import {IJBDirectory} from "@bananapus/core/src/interfaces/IJBDirectory.sol"; +import {IJBRulesets} from "@bananapus/core/src/interfaces/IJBRulesets.sol"; +import {JBMetadataResolver} from "@bananapus/core/src/libraries/JBMetadataResolver.sol"; import {JBRulesetMetadataResolver} from "@bananapus/core/src/libraries/JBRulesetMetadataResolver.sol"; -import {JBBeforeRedeemRecordedContext} from "@bananapus/core/src/structs/JBBeforeRedeemRecordedContext.sol"; import {JBAfterPayRecordedContext} from "@bananapus/core/src/structs/JBAfterPayRecordedContext.sol"; +import {JBBeforeRedeemRecordedContext} from "@bananapus/core/src/structs/JBBeforeRedeemRecordedContext.sol"; import {JBRuleset} from "@bananapus/core/src/structs/JBRuleset.sol"; -import {JBMetadataResolver} from "@bananapus/core/src/libraries/JBMetadataResolver.sol"; +import {JBOwnable} from "@bananapus/ownable/src/JBOwnable.sol"; import {JBPermissionIds} from "@bananapus/permission-ids/src/JBPermissionIds.sol"; +import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol"; +import {Context} from "@openzeppelin/contracts/utils/Context.sol"; +import {mulDiv} from "@prb/math/src/Common.sol"; import {JB721Hook} from "./abstract/JB721Hook.sol"; import {IJB721TiersHook} from "./interfaces/IJB721TiersHook.sol"; -import {IJB721TokenUriResolver} from "./interfaces/IJB721TokenUriResolver.sol"; import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol"; -import {JBIpfsDecoder} from "./libraries/JBIpfsDecoder.sol"; +import {IJB721TokenUriResolver} from "./interfaces/IJB721TokenUriResolver.sol"; import {JB721TiersRulesetMetadataResolver} from "./libraries/JB721TiersRulesetMetadataResolver.sol"; -import {JB721TierConfig} from "./structs/JB721TierConfig.sol"; +import {JBIpfsDecoder} from "./libraries/JBIpfsDecoder.sol"; import {JB721Tier} from "./structs/JB721Tier.sol"; -import {JB721TiersHookFlags} from "./structs/JB721TiersHookFlags.sol"; +import {JB721TierConfig} from "./structs/JB721TierConfig.sol"; +import {JB721TiersSetDiscountPercentConfig} from "./structs/JB721TiersSetDiscountPercentConfig.sol"; import {JB721InitTiersConfig} from "./structs/JB721InitTiersConfig.sol"; +import {JB721TiersHookFlags} from "./structs/JB721TiersHookFlags.sol"; import {JB721TiersMintReservesConfig} from "./structs/JB721TiersMintReservesConfig.sol"; /// @title JB721TiersHook @@ -39,9 +39,35 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook // --------------------------- custom errors ------------------------- // //*********************************************************************// - error OVERSPENDING(); - error MINT_RESERVE_NFTS_PAUSED(); - error TIER_TRANSFERS_PAUSED(); + error JB721TiersHook_AlreadyInitialized(); + error JB721TiersHook_Overspending(); + error JB721TiersHook_MintReserveNftsPaused(); + error JB721TiersHook_TierTransfersPaused(); + + //*********************************************************************// + // --------------- public immutable stored properties ---------------- // + //*********************************************************************// + + /// @notice The contract storing and managing project rulesets. + IJBRulesets public immutable override RULESETS; + + /// @notice The contract that stores and manages data for this contract's NFTs. + IJB721TiersHookStore public immutable override STORE; + + //*********************************************************************// + // ---------------------- public stored properties ------------------- // + //*********************************************************************// + /// @notice The base URI for the NFT `tokenUris`. + string public override baseURI; + + /// @notice This contract's metadata URI. + string public override contractURI; + + /// @notice If an address pays more than the price of the NFT they received, the extra amount is stored as credits + /// which can be redeemed to mint NFTs. + /// @custom:param addr The address to get the NFT credits balance of. + /// @return The amount of credits the address has. + mapping(address addr => uint256) public override payCreditsOf; //*********************************************************************// // --------------------- internal stored properties ------------------ // @@ -59,28 +85,28 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook uint256 internal _packedPricingContext; //*********************************************************************// - // --------------------- public stored properties -------------------- // + // -------------------------- constructor ---------------------------- // //*********************************************************************// - /// @notice The contract that stores and manages data for this contract's NFTs. - /// @dev Set once in initializer. - IJB721TiersHookStore public override STORE; - - /// @notice The contract storing and managing project rulesets. - /// @dev Set once in initializer. - IJBRulesets public override RULESETS; - - /// @notice If an address pays more than the price of the NFT they received, the extra amount is stored as credits - /// which can be redeemed to mint NFTs. - /// @custom:param addr The address to get the NFT credits balance of. - /// @return The amount of credits the address has. - mapping(address addr => uint256) public override payCreditsOf; - - /// @notice The base URI for the NFT `tokenUris`. - string public override baseURI; - - /// @notice This contract's metadata URI. - string public override contractURI; + /// @param directory A directory of terminals and controllers for projects. + /// @param permissions A contract storing permissions. + /// @param rulesets A contract storing and managing project rulesets. + /// @param store The contract which stores the NFT's data. + /// @param trustedForwarder The trusted forwarder for the ERC2771Context. + constructor( + IJBDirectory directory, + IJBPermissions permissions, + IJBRulesets rulesets, + IJB721TiersHookStore store, + address trustedForwarder + ) + JBOwnable(directory.PROJECTS(), permissions, msg.sender, uint88(0)) + JB721Hook(directory) + ERC2771Context(trustedForwarder) + { + RULESETS = rulesets; + STORE = store; + } //*********************************************************************// // ------------------------- external views -------------------------- // @@ -128,117 +154,35 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook return STORE.balanceOf(address(this), owner); } - /// @notice The metadata URI of the NFT with the specified token ID. - /// @dev Defers to the `tokenUriResolver` if it is set. Otherwise, use the `tokenUri` corresponding with the NFT's - /// tier. - /// @param tokenId The token ID of the NFT to get the metadata URI of. - /// @return The token URI from the `tokenUriResolver` if it is set. If it isn't set, the token URI for the NFT's - /// tier. - function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { - // Keep a reference to the store. - IJB721TiersHookStore store = STORE; - - // Get a reference to the `tokenUriResolver`. - IJB721TokenUriResolver resolver = store.tokenUriResolverOf(address(this)); - - // If a `tokenUriResolver` is set, use it to resolve the token URI. - if (address(resolver) != address(0)) return resolver.tokenUriOf(address(this), tokenId); - - // Otherwise, return the token URI corresponding with the NFT's tier. - return JBIpfsDecoder.decode(baseURI, store.encodedTierIPFSUriOf(address(this), tokenId)); - } - - /// @notice The combined redemption weight of the NFTs with the specified token IDs. - /// @dev An NFT's redemption weight is its price. - /// @dev To get their relative redemption weight, divide the result by the `totalRedemptionWeight(...)`. - /// @param tokenIds The token IDs of the NFTs to get the cumulative redemption weight of. - /// @return weight The redemption weight of the tokenIds. - function redemptionWeightOf( - uint256[] memory tokenIds, - JBBeforeRedeemRecordedContext calldata - ) - public - view - virtual - override - returns (uint256) - { - return STORE.redemptionWeightOf(address(this), tokenIds); - } - - /// @notice The combined redemption weight of all outstanding NFTs. - /// @dev An NFT's redemption weight is its price. - /// @return weight The total redemption weight. - function totalRedemptionWeight(JBBeforeRedeemRecordedContext calldata) - public - view - virtual - override - returns (uint256) - { - return STORE.totalRedemptionWeight(address(this)); - } - - /// @notice Indicates if this contract adheres to the specified interface. - /// @dev See {IERC165-supportsInterface}. - /// @param interfaceId The ID of the interface to check for adherence to. - function supportsInterface(bytes4 interfaceId) public view override returns (bool) { - return interfaceId == type(IJB721TiersHook).interfaceId || super.supportsInterface(interfaceId); - } - - //*********************************************************************// - // -------------------------- constructor ---------------------------- // - //*********************************************************************// - - /// @param directory A directory of terminals and controllers for projects. - /// @param permissions A contract storing permissions. - /// @param trustedForwarder The trusted forwarder for the ERC2771Context. - constructor( - IJBDirectory directory, - IJBPermissions permissions, - address trustedForwarder - ) - JBOwnable(directory.PROJECTS(), permissions, msg.sender, uint88(0)) - JB721Hook(directory) - ERC2771Context(trustedForwarder) - {} - /// @notice Initializes a cloned copy of the original `JB721Hook` contract. /// @param projectId The ID of the project this this hook is associated with. /// @param name The name of the NFT collection. /// @param symbol The symbol representing the NFT collection. - /// @param rulesets A contract storing and managing project rulesets. /// @param baseUri The URI to use as a base for full NFT `tokenUri`s. /// @param tokenUriResolver An optional contract responsible for resolving the token URI for each NFT's token ID. /// @param contractUri A URI where this contract's metadata can be found. /// @param tiersConfig The NFT tiers and pricing context to initialize the hook with. The tiers must be sorted by /// price (from least to greatest). - /// @param store The contract which stores the NFT's data. /// @param flags A set of additional options which dictate how the hook behaves. function initialize( uint256 projectId, string memory name, string memory symbol, - IJBRulesets rulesets, string memory baseUri, IJB721TokenUriResolver tokenUriResolver, string memory contractUri, JB721InitTiersConfig memory tiersConfig, - IJB721TiersHookStore store, JB721TiersHookFlags memory flags ) public override { - // Stop re-initialization. - if (address(STORE) != address(0)) revert(); + // Stop re-initialization by ensuring a projectId is provided and doesn't already exist. + if (PROJECT_ID != 0 || projectId == 0) revert JB721TiersHook_AlreadyInitialized(); // Initialize the superclass. JB721Hook._initialize(projectId, name, symbol); - RULESETS = rulesets; - STORE = store; - // Pack pricing context from the `tiersConfig`. uint256 packed; // pack the currency in bits 0-31 (32 bits). @@ -262,24 +206,124 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook // Set the token URI resolver if provided. if (tokenUriResolver != IJB721TokenUriResolver(address(0))) { - store.recordSetTokenUriResolver(tokenUriResolver); + _recordSetTokenUriResolver(tokenUriResolver); } // Record the tiers in this hook's store. // slither-disable-next-line unused-return - if (tiersConfig.tiers.length != 0) store.recordAddTiers(tiersConfig.tiers); + if (tiersConfig.tiers.length != 0) STORE.recordAddTiers(tiersConfig.tiers); // Set the flags if needed. if ( flags.noNewTiersWithReserves || flags.noNewTiersWithVotes || flags.noNewTiersWithOwnerMinting || flags.preventOverspending - ) store.recordFlags(flags); + ) STORE.recordFlags(flags); + + // Transfer ownership to the initializer. + _transferOwnership(_msgSender()); + } + + /// @notice The combined redemption weight of the NFTs with the specified token IDs. + /// @dev An NFT's redemption weight is its price. + /// @dev To get their relative redemption weight, divide the result by the `totalRedemptionWeight(...)`. + /// @param tokenIds The token IDs of the NFTs to get the cumulative redemption weight of. + /// @return weight The redemption weight of the tokenIds. + function redemptionWeightOf( + uint256[] memory tokenIds, + JBBeforeRedeemRecordedContext calldata + ) + public + view + virtual + override + returns (uint256) + { + return STORE.redemptionWeightOf(address(this), tokenIds); + } + + /// @notice Indicates if this contract adheres to the specified interface. + /// @dev See {IERC165-supportsInterface}. + /// @param interfaceId The ID of the interface to check for adherence to. + function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + return interfaceId == type(IJB721TiersHook).interfaceId || super.supportsInterface(interfaceId); + } + + /// @notice The metadata URI of the NFT with the specified token ID. + /// @dev Defers to the `tokenUriResolver` if it is set. Otherwise, use the `tokenUri` corresponding with the NFT's + /// tier. + /// @param tokenId The token ID of the NFT to get the metadata URI of. + /// @return The token URI from the `tokenUriResolver` if it is set. If it isn't set, the token URI for the NFT's + /// tier. + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + // Get a reference to the `tokenUriResolver`. + IJB721TokenUriResolver resolver = STORE.tokenUriResolverOf(address(this)); + + // If a `tokenUriResolver` is set, use it to resolve the token URI. + if (address(resolver) != address(0)) return resolver.tokenUriOf(address(this), tokenId); + + // Otherwise, return the token URI corresponding with the NFT's tier. + return JBIpfsDecoder.decode(baseURI, STORE.encodedTierIPFSUriOf(address(this), tokenId)); + } + + /// @notice The combined redemption weight of all outstanding NFTs. + /// @dev An NFT's redemption weight is its price. + /// @return weight The total redemption weight. + function totalRedemptionWeight( + JBBeforeRedeemRecordedContext calldata + ) + public + view + virtual + override + returns (uint256) + { + return STORE.totalRedemptionWeight(address(this)); } //*********************************************************************// // ---------------------- external transactions ---------------------- // //*********************************************************************// + /// @notice Add or delete tiers. + /// @dev Only the contract's owner or an operator with the `ADJUST_TIERS` permission from the owner can adjust the + /// tiers. + /// @dev Any added tiers must adhere to this hook's `JB721TiersHookFlags`. + /// @param tiersToAdd The tiers to add, as an array of `JB721TierConfig` structs`. + /// @param tierIdsToRemove The tiers to remove, as an array of tier IDs. + function adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata tierIdsToRemove) external override { + // Enforce permissions. + _requirePermissionFrom({account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.ADJUST_721_TIERS}); + + // Get a reference to the number of tiers being added. + uint256 numberOfTiersToAdd = tiersToAdd.length; + + // Get a reference to the number of tiers being removed. + uint256 numberOfTiersToRemove = tierIdsToRemove.length; + + // Remove the tiers. + if (numberOfTiersToRemove != 0) { + // Emit events for each removed tier. + for (uint256 i; i < numberOfTiersToRemove; i++) { + emit RemoveTier({tierId: tierIdsToRemove[i], caller: _msgSender()}); + } + + // Record the removed tiers. + // slither-disable-next-line reentrancy-events + STORE.recordRemoveTierIds(tierIdsToRemove); + } + + // Add the tiers. + if (numberOfTiersToAdd != 0) { + // Record the added tiers in the store. + uint256[] memory tierIdsAdded = STORE.recordAddTiers(tiersToAdd); + + // Emit events for each added tier. + for (uint256 i; i < numberOfTiersToAdd; i++) { + emit AddTier({tierId: tierIdsAdded[i], tier: tiersToAdd[i], caller: _msgSender()}); + } + } + } + /// @notice Manually mint NFTs from the provided tiers . /// @param tierIds The IDs of the tiers to mint from. /// @param beneficiary The address to mint to. @@ -304,20 +348,25 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook }); // Keep a reference to the number of NFTs being minted. - uint256 numberOfNfts = tierIds.length; + uint256 numberOfTiers = tierIds.length; // Keep a reference to the token ID being iterated upon. uint256 tokenId; - for (uint256 i; i < numberOfNfts; i++) { + for (uint256 i; i < numberOfTiers; i++) { // Set the token ID. tokenId = tokenIds[i]; - emit Mint(tokenId, tierIds[i], beneficiary, 0, _msgSender()); - // Mint the NFT. - // slither-disable-next-line reentrancy-events _mint(beneficiary, tokenId); + + emit Mint({ + tokenId: tokenId, + tierId: tierIds[i], + beneficiary: beneficiary, + totalAmountPaid: 0, + caller: _msgSender() + }); } } @@ -337,47 +386,43 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook } } - /// @notice Add or delete tiers. - /// @dev Only the contract's owner or an operator with the `ADJUST_TIERS` permission from the owner can adjust the + /// @notice Allows the collection's owner to set the discount for a tier, if the tier allows it. + /// @dev Only the contract's owner or an operator with the `SET_721_DISCOUNT_PERCENT` permission from the owner can + /// adjust the /// tiers. - /// @dev Any added tiers must adhere to this hook's `JB721TiersHookFlags`. - /// @param tiersToAdd The tiers to add, as an array of `JB721TierConfig` structs`. - /// @param tierIdsToRemove The tiers to remove, as an array of tier IDs. - function adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata tierIdsToRemove) external override { + /// @param tierId The ID of the tier to set the discount of. + /// @param discountPercent The discount percent to set. + function setDiscountPercentOf(uint256 tierId, uint256 discountPercent) external override { // Enforce permissions. - _requirePermissionFrom({account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.ADJUST_721_TIERS}); - - // Get a reference to the number of tiers being added. - uint256 numberOfTiersToAdd = tiersToAdd.length; - - // Get a reference to the number of tiers being removed. - uint256 numberOfTiersToRemove = tierIdsToRemove.length; + _requirePermissionFrom({ + account: owner(), + projectId: PROJECT_ID, + permissionId: JBPermissionIds.SET_721_DISCOUNT_PERCENT + }); + _setDiscountPercentOf(tierId, discountPercent); + } - // Keep a reference to the store. - IJB721TiersHookStore store = STORE; + /// @notice Allows the collection's owner to set the discount percent for multiple tiers. + /// @param configs The configs to set the discount percent for. + function setDiscountPercentsOf(JB721TiersSetDiscountPercentConfig[] calldata configs) external override { + // Enforce permissions. + _requirePermissionFrom({ + account: owner(), + projectId: PROJECT_ID, + permissionId: JBPermissionIds.SET_721_DISCOUNT_PERCENT + }); - // Remove the tiers. - if (numberOfTiersToRemove != 0) { - // Emit events for each removed tier. - for (uint256 i; i < numberOfTiersToRemove; i++) { - emit RemoveTier(tierIdsToRemove[i], _msgSender()); - } + // Keep a reference to the number of configs being set. + uint256 numberOfConfigs = configs.length; - // Record the removed tiers. - // slither-disable-next-line reentrancy-events - store.recordRemoveTierIds(tierIdsToRemove); - } + // Keep a reference to the config being iterated on. + JB721TiersSetDiscountPercentConfig memory config; - // Add the tiers. - if (numberOfTiersToAdd != 0) { - // Record the added tiers in the store. - // slither-disable-next-line reentrancy-events - uint256[] memory tierIdsAdded = store.recordAddTiers(tiersToAdd); + for (uint256 i; i < numberOfConfigs; i++) { + // Set the config being iterated on. + config = configs[i]; - // Emit events for each added tier. - for (uint256 i; i < numberOfTiersToAdd; i++) { - emit AddTier(tierIdsAdded[i], tiersToAdd[i], _msgSender()); - } + _setDiscountPercentOf(config.tierId, config.discountPercent); } } @@ -404,29 +449,24 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook if (bytes(baseUri).length != 0) { // Store the new base URI. baseURI = baseUri; - emit SetBaseUri(baseUri, _msgSender()); + emit SetBaseUri({baseUri: baseUri, caller: _msgSender()}); } if (bytes(contractUri).length != 0) { // Store the new contract URI. contractURI = contractUri; - emit SetContractUri(contractUri, _msgSender()); + emit SetContractUri({uri: contractUri, caller: _msgSender()}); } - // Keep a reference to the store. - IJB721TiersHookStore store = STORE; - if (tokenUriResolver != IJB721TokenUriResolver(address(this))) { - emit SetTokenUriResolver(tokenUriResolver, _msgSender()); - // Store the new URI resolver. // slither-disable-next-line reentrancy-events - store.recordSetTokenUriResolver(tokenUriResolver); + _recordSetTokenUriResolver(tokenUriResolver); } if (encodedIPFSTUriTierId != 0 && encodedIPFSUri != bytes32(0)) { - emit SetEncodedIPFSUri(encodedIPFSTUriTierId, encodedIPFSUri, _msgSender()); + emit SetEncodedIPFSUri({tierId: encodedIPFSTUriTierId, encodedUri: encodedIPFSUri, caller: _msgSender()}); // Store the new encoded IPFS URI. - store.recordSetEncodedIPFSUriOf(encodedIPFSTUriTierId, encodedIPFSUri); + STORE.recordSetEncodedIPFSUriOf(encodedIPFSTUriTierId, encodedIPFSUri); } } @@ -440,25 +480,21 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook /// @param count The number of reserved NFTs to mint. function mintPendingReservesFor(uint256 tierId, uint256 count) public override { // Get a reference to the project's current ruleset. - // slither-disable-next-line calls-loop - JBRuleset memory ruleset = RULESETS.currentOf(PROJECT_ID); + JBRuleset memory ruleset = _currentRulesetOf(PROJECT_ID); // Pending reserve mints must not be paused. if (JB721TiersRulesetMetadataResolver.mintPendingReservesPaused((JBRulesetMetadataResolver.metadata(ruleset)))) { - revert MINT_RESERVE_NFTS_PAUSED(); + revert JB721TiersHook_MintReserveNftsPaused(); } - // Keep a reference to the store. - IJB721TiersHookStore store = STORE; - // Record the reserved mint for the tier. // slither-disable-next-line reentrancy-events,calls-loop - uint256[] memory tokenIds = store.recordMintReservesFor(tierId, count); + uint256[] memory tokenIds = STORE.recordMintReservesFor(tierId, count); // Keep a reference to the beneficiary. - // slither-disable-next-line reentrancy-events,calls-loop - address reserveBeneficiary = store.reserveBeneficiaryOf(address(this), tierId); + // slither-disable-next-line calls-loop + address reserveBeneficiary = STORE.reserveBeneficiaryOf(address(this), tierId); // Keep a reference to the token ID being iterated upon. uint256 tokenId; @@ -467,7 +503,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook // Set the token ID. tokenId = tokenIds[i]; - emit MintReservedNft(tokenId, tierId, reserveBeneficiary, _msgSender()); + emit MintReservedNft({ + tokenId: tokenId, + tierId: tierId, + beneficiary: reserveBeneficiary, + caller: _msgSender() + }); // Mint the NFT. // slither-disable-next-line reentrency-events @@ -479,6 +520,88 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook // ------------------------ internal functions ----------------------- // //*********************************************************************// + /// @dev ERC-2771 specifies the context as being a single address (20 bytes). + function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { + return super._contextSuffixLength(); + } + + /// @notice The project's current ruleset. + /// @param projectId The ID of the project to check. + /// @return The project's current ruleset. + function _currentRulesetOf(uint256 projectId) internal view returns (JBRuleset memory) { + // slither-disable-next-line calls-loop + return RULESETS.currentOf(projectId); + } + + /// @notice A function which gets called after NFTs have been redeemed and recorded by the terminal. + /// @param tokenIds The token IDs of the NFTs that were burned. + function _didBurn(uint256[] memory tokenIds) internal virtual override { + // Add to burned counter. + STORE.recordBurn(tokenIds); + } + + /// @notice Mints one NFT from each of the specified tiers for the beneficiary. + /// @dev The same tier can be specified more than once. + /// @param amount The amount to base the mints on. The total price of the NFTs being minted cannot be larger than + /// this amount. + /// @param mintTierIds An array of NFT tier IDs to be minted. + /// @param beneficiary The address receiving the newly minted NFTs. + /// @return leftoverAmount The `amount` leftover after minting. + function _mintAll( + uint256 amount, + uint16[] memory mintTierIds, + address beneficiary + ) + internal + returns (uint256 leftoverAmount) + { + // Keep a reference to the NFT token IDs. + uint256[] memory tokenIds; + + // Record the NFT mints. The token IDs returned correspond to the tier IDs passed in. + (tokenIds, leftoverAmount) = STORE.recordMint({ + amount: amount, + tierIds: mintTierIds, + isOwnerMint: false // Not a manual mint + }); + + // Get a reference to the number of NFTs being minted. + uint256 mintsLength = tokenIds.length; + + // Keep a reference to the token ID being iterated on. + uint256 tokenId; + + // Loop through each token ID and mint the corresponding NFT. + for (uint256 i; i < mintsLength; i++) { + // Get a reference to the token ID being iterated on. + tokenId = tokenIds[i]; + + emit Mint({ + tokenId: tokenId, + tierId: mintTierIds[i], + beneficiary: beneficiary, + totalAmountPaid: amount, + caller: _msgSender() + }); + + // Mint the NFT. + // slither-disable-next-line reentrancy-events + _mint(beneficiary, tokenId); + } + } + + /// @notice Returns the calldata, prefered to use over `msg.data` + /// @return calldata the `msg.data` of this call + function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) { + return ERC2771Context._msgData(); + } + + /// @notice Returns the sender, prefered to use over `msg.sender` + /// @return sender the sender address of this call. + function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) { + return ERC2771Context._msgSender(); + } + /// @notice Process a payment, minting NFTs and updating credits as necessary. /// @param context Payment context provided by the terminal after it has recorded the payment in the terminal store. function _processPayment(JBAfterPayRecordedContext calldata context) internal virtual override { @@ -531,8 +654,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook } // Keep a reference to the boolean indicating whether paying more than the price of the NFTs being minted is - // allowed. Defaults to false. - bool allowOverspending; + // allowed. Defaults to the collection's flag. + bool allowOverspending = !STORE.flagsOf(address(this)).preventOverspending; // Resolve the metadata. (bool found, bytes memory metadata) = @@ -542,11 +665,14 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook // Keep a reference to the IDs of the tier be to minted. uint16[] memory tierIdsToMint; + // Keep a reference to the payer's flag indicating whether overspending is allowed. + bool payerAllowsOverspending; + // Decode the metadata. - (allowOverspending, tierIdsToMint) = abi.decode(metadata, (bool, uint16[])); + (payerAllowsOverspending, tierIdsToMint) = abi.decode(metadata, (bool, uint16[])); // Make sure overspending is allowed if requested. - if (allowOverspending && STORE.flagsOf(address(this)).preventOverspending) { + if (allowOverspending && !payerAllowsOverspending) { allowOverspending = false; } @@ -556,14 +682,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook leftoverAmount = _mintAll({amount: leftoverAmount, mintTierIds: tierIdsToMint, beneficiary: context.beneficiary}); } - } else if (!STORE.flagsOf(address(this)).preventOverspending) { - allowOverspending = true; } // If overspending is allowed and there are leftover funds, add those funds to the beneficiary's NFT credits. if (leftoverAmount != 0) { // If overspending isn't allowed, revert. - if (!allowOverspending) revert OVERSPENDING(); + if (!allowOverspending) revert JB721TiersHook_Overspending(); // Increment the leftover amount. unchecked { @@ -572,9 +696,19 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook // Emit the change in NFT credits. if (newPayCredits > payCredits) { - emit AddPayCredits(newPayCredits - payCredits, newPayCredits, context.beneficiary, _msgSender()); + emit AddPayCredits({ + amount: newPayCredits - payCredits, + newTotalCredits: newPayCredits, + account: context.beneficiary, + caller: _msgSender() + }); } else if (payCredits > newPayCredits) { - emit UsePayCredits(payCredits - newPayCredits, newPayCredits, context.beneficiary, _msgSender()); + emit UsePayCredits({ + amount: payCredits - newPayCredits, + newTotalCredits: newPayCredits, + account: context.beneficiary, + caller: _msgSender() + }); } // Store the new NFT credits for the beneficiary. @@ -583,75 +717,44 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook // Otherwise, reset their NFT credits. } else if (payCredits != unusedPayCredits) { // Emit the change in NFT credits. - emit UsePayCredits(payCredits - unusedPayCredits, unusedPayCredits, context.beneficiary, _msgSender()); + emit UsePayCredits({ + amount: payCredits - unusedPayCredits, + newTotalCredits: unusedPayCredits, + account: context.beneficiary, + caller: _msgSender() + }); // Store the new NFT credits. payCreditsOf[context.beneficiary] = unusedPayCredits; } } - /// @notice A function which gets called after NFTs have been redeemed and recorded by the terminal. - /// @param tokenIds The token IDs of the NFTs that were burned. - function _didBurn(uint256[] memory tokenIds) internal virtual override { - // Add to burned counter. - STORE.recordBurn(tokenIds); - } - - /// @notice Mints one NFT from each of the specified tiers for the beneficiary. - /// @dev The same tier can be specified more than once. - /// @param amount The amount to base the mints on. The total price of the NFTs being minted cannot be larger than - /// this amount. - /// @param mintTierIds An array of NFT tier IDs to be minted. - /// @param beneficiary The address receiving the newly minted NFTs. - /// @return leftoverAmount The `amount` leftover after minting. - function _mintAll( - uint256 amount, - uint16[] memory mintTierIds, - address beneficiary - ) - internal - returns (uint256 leftoverAmount) - { - // Keep a reference to the NFT token IDs. - uint256[] memory tokenIds; - - // Record the NFT mints. The token IDs returned correspond to the tier IDs passed in. - // slither-disable-next-line reentrency-events - (tokenIds, leftoverAmount) = STORE.recordMint({ - amount: amount, - tierIds: mintTierIds, - isOwnerMint: false // Not a manual mint - }); + /// @notice Record the setting of a new token URI resolver. + /// @param tokenUriResolver The new token URI resolver. + function _recordSetTokenUriResolver(IJB721TokenUriResolver tokenUriResolver) internal { + emit SetTokenUriResolver({resolver: tokenUriResolver, caller: _msgSender()}); - // Get a reference to the number of NFTs being minted. - uint256 mintsLength = tokenIds.length; - - // Keep a reference to the token ID being iterated on. - uint256 tokenId; - - // Loop through each token ID and mint the corresponding NFT. - for (uint256 i; i < mintsLength; i++) { - // Get a reference to the token ID being iterated on. - tokenId = tokenIds[i]; + STORE.recordSetTokenUriResolver(tokenUriResolver); + } - emit Mint(tokenId, mintTierIds[i], beneficiary, amount, _msgSender()); + /// @notice Internal function to set the discount percent for a tier. + /// @param tierId The ID of the tier to set the discount percent for. + /// @param discountPercent The discount percent to set for the tier. + function _setDiscountPercentOf(uint256 tierId, uint256 discountPercent) internal { + emit SetDiscountPercent({tierId: tierId, discountPercent: discountPercent, caller: _msgSender()}); - // Mint the NFT. - // slither-disable-next-line reentrancy-events - _mint(beneficiary, tokenId); - } + // Record the discount percent for the tier. + // slither-disable-next-line calls-loop + STORE.recordSetDiscountPercentOf({tierId: tierId, discountPercent: discountPercent}); } /// @notice Before transferring an NFT, register its first owner (if necessary). /// @param to The address the NFT is being transferred to. /// @param tokenId The token ID of the NFT being transferred. function _update(address to, uint256 tokenId, address auth) internal virtual override returns (address from) { - // Keep a reference to the store. - IJB721TiersHookStore store = STORE; - // Get a reference to the tier. // slither-disable-next-line calls-loop - JB721Tier memory tier = store.tierOfTokenId({hook: address(this), tokenId: tokenId, includeResolvedUri: false}); + JB721Tier memory tier = STORE.tierOfTokenId({hook: address(this), tokenId: tokenId, includeResolvedUri: false}); // Record the transfers and keep a reference to where the token is coming from. from = super._update(to, tokenId, auth); @@ -661,14 +764,13 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook // If transfers are pausable, check if they're paused. if (tier.transfersPausable) { // Get a reference to the project's current ruleset. - // slither-disable-next-line calls-loop - JBRuleset memory ruleset = RULESETS.currentOf(PROJECT_ID); + JBRuleset memory ruleset = _currentRulesetOf(PROJECT_ID); // If transfers are paused and the NFT isn't being transferred to the zero address, revert. if ( to != address(0) && JB721TiersRulesetMetadataResolver.transfersPaused((JBRulesetMetadataResolver.metadata(ruleset))) - ) revert TIER_TRANSFERS_PAUSED(); + ) revert JB721TiersHook_TierTransfersPaused(); } // If the token isn't already associated with a first owner, store the sender as the first owner. @@ -678,23 +780,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook // Record the transfer. // slither-disable-next-line reentrency-events,calls-loop - store.recordTransferForTier(tier.id, from, to); - } - - /// @notice Returns the sender, prefered to use over `msg.sender` - /// @return sender the sender address of this call. - function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) { - return ERC2771Context._msgSender(); - } - - /// @notice Returns the calldata, prefered to use over `msg.data` - /// @return calldata the `msg.data` of this call - function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) { - return ERC2771Context._msgData(); - } - - /// @dev ERC-2771 specifies the context as being a single address (20 bytes). - function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) { - return super._contextSuffixLength(); + STORE.recordTransferForTier(tier.id, from, to); } } diff --git a/src/JB721TiersHookDeployer.sol b/src/JB721TiersHookDeployer.sol index a458bd40..0d5cdf23 100644 --- a/src/JB721TiersHookDeployer.sol +++ b/src/JB721TiersHookDeployer.sol @@ -1,39 +1,39 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {IJBAddressRegistry} from "@bananapus/address-registry/src/interfaces/IJBAddressRegistry.sol"; import {JBOwnable} from "@bananapus/ownable/src/JBOwnable.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol"; +import {JB721TiersHook} from "./JB721TiersHook.sol"; import {IJB721TiersHookDeployer} from "./interfaces/IJB721TiersHookDeployer.sol"; import {IJB721TiersHook} from "./interfaces/IJB721TiersHook.sol"; -import {JBDeploy721TiersHookConfig} from "./structs/JBDeploy721TiersHookConfig.sol"; import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol"; -import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol"; -import {JB721TiersHook} from "./JB721TiersHook.sol"; +import {JBDeploy721TiersHookConfig} from "./structs/JBDeploy721TiersHookConfig.sol"; /// @title JB721TiersHookDeployer /// @notice Deploys a `JB721TiersHook` for an existing project. contract JB721TiersHookDeployer is ERC2771Context, IJB721TiersHookDeployer { - //*********************************************************************// - // ----------------------- internal properties ----------------------- // - //*********************************************************************// - - /// @notice This contract's current nonce, used for the Juicebox address registry. - uint256 internal _nonce; - //*********************************************************************// // --------------- public immutable stored properties ---------------- // //*********************************************************************// + /// @notice A registry which stores references to contracts and their deployers. + IJBAddressRegistry public immutable ADDRESS_REGISTRY; + /// @notice A 721 tiers hook. JB721TiersHook public immutable HOOK; /// @notice The contract that stores and manages data for this contract's NFTs. IJB721TiersHookStore public immutable STORE; - /// @notice A registry which stores references to contracts and their deployers. - IJBAddressRegistry public immutable ADDRESS_REGISTRY; + //*********************************************************************// + // ----------------------- internal properties ----------------------- // + //*********************************************************************// + + /// @notice This contract's current nonce, used for the Juicebox address registry. + uint256 internal _nonce; //*********************************************************************// // -------------------------- constructor ---------------------------- // @@ -64,7 +64,7 @@ contract JB721TiersHookDeployer is ERC2771Context, IJB721TiersHookDeployer { /// @return newHook The address of the newly deployed hook. function deployHookFor( uint256 projectId, - JBDeploy721TiersHookConfig memory deployTiersHookConfig + JBDeploy721TiersHookConfig calldata deployTiersHookConfig ) external override @@ -73,18 +73,16 @@ contract JB721TiersHookDeployer is ERC2771Context, IJB721TiersHookDeployer { // Deploy the governance variant specified by the config. newHook = IJB721TiersHook(Clones.clone(address(HOOK))); - emit HookDeployed(projectId, newHook); + emit HookDeployed({projectId: projectId, hook: newHook, caller: msg.sender}); newHook.initialize({ projectId: projectId, name: deployTiersHookConfig.name, symbol: deployTiersHookConfig.symbol, - rulesets: deployTiersHookConfig.rulesets, baseUri: deployTiersHookConfig.baseUri, tokenUriResolver: deployTiersHookConfig.tokenUriResolver, contractUri: deployTiersHookConfig.contractUri, tiersConfig: deployTiersHookConfig.tiersConfig, - store: STORE, flags: deployTiersHookConfig.flags }); diff --git a/src/JB721TiersHookProjectDeployer.sol b/src/JB721TiersHookProjectDeployer.sol index c1eb54e4..521c18b7 100644 --- a/src/JB721TiersHookProjectDeployer.sol +++ b/src/JB721TiersHookProjectDeployer.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {JBOwnable} from "@bananapus/ownable/src/JBOwnable.sol"; import {JBPermissioned} from "@bananapus/core/src/abstract/JBPermissioned.sol"; -import {IJBDirectory} from "@bananapus/core/src/interfaces/IJBDirectory.sol"; import {IJBController} from "@bananapus/core/src/interfaces/IJBController.sol"; +import {IJBDirectory} from "@bananapus/core/src/interfaces/IJBDirectory.sol"; import {IJBPermissions} from "@bananapus/core/src/interfaces/IJBPermissions.sol"; -import {JBPermissionIds} from "@bananapus/permission-ids/src/JBPermissionIds.sol"; import {JBRulesetConfig} from "@bananapus/core/src/structs/JBRulesetConfig.sol"; import {JBRulesetMetadata} from "@bananapus/core/src/structs/JBRulesetMetadata.sol"; +import {JBOwnable} from "@bananapus/ownable/src/JBOwnable.sol"; +import {JBPermissionIds} from "@bananapus/permission-ids/src/JBPermissionIds.sol"; import {IJB721TiersHookDeployer} from "./interfaces/IJB721TiersHookDeployer.sol"; import {IJB721TiersHookProjectDeployer} from "./interfaces/IJB721TiersHookProjectDeployer.sol"; @@ -65,8 +65,8 @@ contract JB721TiersHookProjectDeployer is JBPermissioned, IJB721TiersHookProject /// @return projectId The ID of the newly launched project. function launchProjectFor( address owner, - JBDeploy721TiersHookConfig memory deployTiersHookConfig, - JBLaunchProjectConfig memory launchProjectConfig, + JBDeploy721TiersHookConfig calldata deployTiersHookConfig, + JBLaunchProjectConfig calldata launchProjectConfig, IJBController controller ) external @@ -96,8 +96,8 @@ contract JB721TiersHookProjectDeployer is JBPermissioned, IJB721TiersHookProject /// @return rulesetId The ID of the successfully created ruleset. function launchRulesetsFor( uint256 projectId, - JBDeploy721TiersHookConfig memory deployTiersHookConfig, - JBLaunchRulesetsConfig memory launchRulesetsConfig, + JBDeploy721TiersHookConfig calldata deployTiersHookConfig, + JBLaunchRulesetsConfig calldata launchRulesetsConfig, IJBController controller ) external @@ -131,8 +131,8 @@ contract JB721TiersHookProjectDeployer is JBPermissioned, IJB721TiersHookProject /// @return rulesetId The ID of the successfully created ruleset. function queueRulesetsOf( uint256 projectId, - JBDeploy721TiersHookConfig memory deployTiersHookConfig, - JBQueueRulesetsConfig memory queueRulesetsConfig, + JBDeploy721TiersHookConfig calldata deployTiersHookConfig, + JBQueueRulesetsConfig calldata queueRulesetsConfig, IJBController controller ) external @@ -207,6 +207,7 @@ contract JB721TiersHookProjectDeployer is JBPermissioned, IJB721TiersHookProject allowSetController: payDataRulesetConfig.metadata.allowSetController, allowAddAccountingContext: payDataRulesetConfig.metadata.allowAddAccountingContext, allowAddPriceFeed: payDataRulesetConfig.metadata.allowAddPriceFeed, + allowCrosschainSuckerExtension: payDataRulesetConfig.metadata.allowCrosschainSuckerExtension, ownerMustSendPayouts: payDataRulesetConfig.metadata.ownerMustSendPayouts, holdFees: payDataRulesetConfig.metadata.holdFees, useTotalSurplusForRedemptions: payDataRulesetConfig.metadata.useTotalSurplusForRedemptions, @@ -280,6 +281,7 @@ contract JB721TiersHookProjectDeployer is JBPermissioned, IJB721TiersHookProject allowSetController: payDataRulesetConfig.metadata.allowSetController, allowAddAccountingContext: payDataRulesetConfig.metadata.allowAddAccountingContext, allowAddPriceFeed: payDataRulesetConfig.metadata.allowAddPriceFeed, + allowCrosschainSuckerExtension: payDataRulesetConfig.metadata.allowCrosschainSuckerExtension, ownerMustSendPayouts: payDataRulesetConfig.metadata.ownerMustSendPayouts, holdFees: payDataRulesetConfig.metadata.holdFees, useTotalSurplusForRedemptions: payDataRulesetConfig.metadata.useTotalSurplusForRedemptions, @@ -351,6 +353,7 @@ contract JB721TiersHookProjectDeployer is JBPermissioned, IJB721TiersHookProject allowSetController: payDataRulesetConfig.metadata.allowSetController, allowAddAccountingContext: payDataRulesetConfig.metadata.allowAddAccountingContext, allowAddPriceFeed: payDataRulesetConfig.metadata.allowAddPriceFeed, + allowCrosschainSuckerExtension: payDataRulesetConfig.metadata.allowCrosschainSuckerExtension, ownerMustSendPayouts: payDataRulesetConfig.metadata.ownerMustSendPayouts, holdFees: payDataRulesetConfig.metadata.holdFees, useTotalSurplusForRedemptions: payDataRulesetConfig.metadata.useTotalSurplusForRedemptions, diff --git a/src/JB721TiersHookStore.sol b/src/JB721TiersHookStore.sol index 808f54d5..1855a9c9 100644 --- a/src/JB721TiersHookStore.sol +++ b/src/JB721TiersHookStore.sol @@ -1,14 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {mulDiv} from "@prb/math/src/Common.sol"; + import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol"; import {IJB721TokenUriResolver} from "./interfaces/IJB721TokenUriResolver.sol"; +import {JB721Constants} from "./libraries/JB721Constants.sol"; import {JBBitmap} from "./libraries/JBBitmap.sol"; -import {JBBitmapWord} from "./structs/JBBitmapWord.sol"; import {JB721Tier} from "./structs/JB721Tier.sol"; import {JB721TierConfig} from "./structs/JB721TierConfig.sol"; -import {JBStored721Tier} from "./structs/JBStored721Tier.sol"; import {JB721TiersHookFlags} from "./structs/JB721TiersHookFlags.sol"; +import {JBBitmapWord} from "./structs/JBBitmapWord.sol"; +import {JBStored721Tier} from "./structs/JBStored721Tier.sol"; /// @title JB721TiersHookStore /// @notice This contract stores and manages data for many `IJB721TiersHook`s and their NFTs. @@ -20,20 +23,22 @@ contract JB721TiersHookStore is IJB721TiersHookStore { // --------------------------- custom errors ------------------------- // //*********************************************************************// - error CANT_MINT_MANUALLY(); - error CANT_REMOVE_TIER(); - error PRICE_EXCEEDS_AMOUNT(); - error INSUFFICIENT_PENDING_RESERVES(); - error INVALID_CATEGORY_SORT_ORDER(); - error INVALID_QUANTITY(); - error INVALID_TIER(); - error MAX_TIERS_EXCEEDED(); - error NO_SUPPLY(); - error INSUFFICIENT_SUPPLY_REMAINING(); - error RESERVE_FREQUENCY_NOT_ALLOWED(); - error MANUAL_MINTING_NOT_ALLOWED(); - error TIER_REMOVED(); - error VOTING_UNITS_NOT_ALLOWED(); + error JB721TiersHookStore_CantMintManually(); + error JB721TiersHookStore_CantRemoveTier(); + error JB721TiersHookStore_DiscountPercentExceedsBounds(); + error JB721TiersHookStore_DiscountPercentIncreaseNotAllowed(); + error JB721TiersHookStore_PriceExceedsAmount(); + error JB721TiersHookStore_InsufficientPendingReserves(); + error JB721TiersHookStore_InvalidCategorySortOrder(); + error JB721TiersHookStore_InvalidQuantity(); + error JB721TiersHookStore_InvalidTier(); + error JB721TiersHookStore_MaxTiersExceeded(); + error JB721TiersHookStore_NoSupply(); + error JB721TiersHookStore_InsufficientSupplyRemaining(); + error JB721TiersHookStore_ReserveFrequencyNotAllowed(); + error JB721TiersHookStore_ManualMintingNotAllowed(); + error JB721TiersHookStore_TierRemoved(); + error JB721TiersHookStore_VotingUnitsNotAllowed(); //*********************************************************************// // -------------------- private constant properties ------------------ // @@ -47,17 +52,28 @@ contract JB721TiersHookStore is IJB721TiersHookStore { // --------------------- public stored properties -------------------- // //*********************************************************************// + /// @notice Returns the default reserve beneficiary for the provided 721 contract. + /// @dev If a tier has a reserve beneficiary set, it will override this value. + /// @custom:param hook The 721 contract to get the default reserve beneficiary of. + mapping(address hook => address) public override defaultReserveBeneficiaryOf; + + /// @notice Returns the encoded IPFS URI for the provided tier ID of the provided 721 contract. + /// @dev Token URIs managed by this contract are stored in 32 bytes, based on stripped down IPFS hashes. + /// @custom:param hook The 721 contract that the tier belongs to. + /// @custom:param tierId The ID of the tier to get the encoded IPFS URI of. + /// @custom:returns The encoded IPFS URI. + mapping(address hook => mapping(uint256 tierId => bytes32)) public override encodedIPFSUriOf; + /// @notice Returns the largest tier ID currently used on the provided 721 contract. /// @dev This may not include the last tier ID if it has been removed. /// @custom:param hook The 721 contract to get the largest tier ID from. mapping(address hook => uint256) public override maxTierIdOf; - /// @notice Returns the number of NFTs which the provided owner address owns from the provided 721 contract and tier - /// ID. - /// @custom:param hook The 721 contract to get the balance from. - /// @custom:param owner The address to get the tier balance of. - /// @custom:param tierId The ID of the tier to get the balance for. - mapping(address hook => mapping(address owner => mapping(uint256 tierId => uint256))) public override tierBalanceOf; + /// @notice Returns the number of NFTs which have been burned from the provided tier ID of the provided 721 + /// contract. + /// @custom:param hook The 721 contract that the tier belongs to. + /// @custom:param tierId The ID of the tier to get the burn count of. + mapping(address hook => mapping(uint256 tierId => uint256)) public override numberOfBurnedFor; /// @notice Returns the number of reserve NFTs which have been minted from the provided tier ID of the provided 721 /// contract. @@ -65,58 +81,32 @@ contract JB721TiersHookStore is IJB721TiersHookStore { /// @custom:param tierId The ID of the tier to get the reserve mint count of. mapping(address hook => mapping(uint256 tierId => uint256)) public override numberOfReservesMintedFor; - /// @notice Returns the number of NFTs which have been burned from the provided tier ID of the provided 721 - /// contract. - /// @custom:param hook The 721 contract that the tier belongs to. - /// @custom:param tierId The ID of the tier to get the burn count of. - mapping(address hook => mapping(uint256 tierId => uint256)) public override numberOfBurnedFor; - - /// @notice Returns the default reserve beneficiary for the provided 721 contract. - /// @dev If a tier has a reserve beneficiary set, it will override this value. - /// @custom:param hook The 721 contract to get the default reserve beneficiary of. - mapping(address hook => address) public override defaultReserveBeneficiaryOf; + /// @notice Returns the number of NFTs which the provided owner address owns from the provided 721 contract and tier + /// ID. + /// @custom:param hook The 721 contract to get the balance from. + /// @custom:param owner The address to get the tier balance of. + /// @custom:param tierId The ID of the tier to get the balance for. + mapping(address hook => mapping(address owner => mapping(uint256 tierId => uint256))) public override tierBalanceOf; /// @notice Returns the custom token URI resolver which overrides the default token URI resolver for the provided /// 721 contract. /// @custom:param hook The 721 contract to get the custom token URI resolver of. mapping(address hook => IJB721TokenUriResolver) public override tokenUriResolverOf; - /// @notice Returns the encoded IPFS URI for the provided tier ID of the provided 721 contract. - /// @dev Token URIs managed by this contract are stored in 32 bytes, based on stripped down IPFS hashes. - /// @custom:param hook The 721 contract that the tier belongs to. - /// @custom:param tierId The ID of the tier to get the encoded IPFS URI of. - /// @custom:returns The encoded IPFS URI. - mapping(address hook => mapping(uint256 tierId => bytes32)) public override encodedIPFSUriOf; - //*********************************************************************// // --------------------- internal stored properties ------------------ // //*********************************************************************// - /// @notice Returns the ID of the tier which comes after the provided tier ID (sorted by price). - /// @dev If empty, assume the next tier ID should come after. - /// @custom:param hook The address of the 721 contract to get the next tier ID from. - /// @custom:param tierId The ID of the tier to get the next tier ID in relation to. - /// @custom:returns The following tier's ID. - mapping(address hook => mapping(uint256 tierId => uint256)) internal _tierIdAfter; - - /// @notice Returns the reserve beneficiary (if there is one) for the provided tier ID on the provided - /// `IJB721TiersHook` contract. - /// @custom:param hook The address of the 721 contract to get the reserve beneficiary from. - /// @custom:param tierId The ID of the tier to get the reserve beneficiary of. - /// @custom:returns The address of the reserved token beneficiary. - mapping(address hook => mapping(uint256 tierId => address)) internal _reserveBeneficiaryOf; - - /// @notice Returns the stored tier of the provided tier ID on the provided `IJB721TiersHook` contract. - /// @custom:param hook The address of the 721 contract to get the tier from. - /// @custom:param tierId The ID of the tier to get. - /// @custom:returns The stored tier, as a `JBStored721Tier` struct. - mapping(address hook => mapping(uint256 tierId => JBStored721Tier)) internal _storedTierOf; - /// @notice Returns the flags which dictate the behavior of the provided `IJB721TiersHook` contract. /// @custom:param hook The address of the 721 contract to get the flags for. /// @custom:returns The flags. mapping(address hook => JB721TiersHookFlags) internal _flagsOf; + /// @notice Return the ID of the last sorted tier from the provided 721 contract. + /// @dev If not set, it is assumed the `maxTierIdOf` is the last sorted tier ID. + /// @custom:param hook The 721 contract to get the last sorted tier ID from. + mapping(address hook => uint256) internal _lastTrackedSortedTierIdOf; + /// @notice Get the bitmap word at the provided depth from the provided 721 contract's tier removal bitmap. /// @dev See `JBBitmap` for more information. /// @custom:param hook The 721 contract to get the bitmap word from. @@ -124,20 +114,144 @@ contract JB721TiersHookStore is IJB721TiersHookStore { /// @custom:returns word The bitmap row's content. mapping(address hook => mapping(uint256 depth => uint256 word)) internal _removedTiersBitmapWordOf; - /// @notice Return the ID of the last sorted tier from the provided 721 contract. - /// @dev If not set, it is assumed the `maxTierIdOf` is the last sorted tier ID. - /// @custom:param hook The 721 contract to get the last sorted tier ID from. - mapping(address hook => uint256) internal _lastTrackedSortedTierIdOf; + /// @notice Returns the reserve beneficiary (if there is one) for the provided tier ID on the provided + /// `IJB721TiersHook` contract. + /// @custom:param hook The address of the 721 contract to get the reserve beneficiary from. + /// @custom:param tierId The ID of the tier to get the reserve beneficiary of. + /// @custom:returns The address of the reserved token beneficiary. + mapping(address hook => mapping(uint256 tierId => address)) internal _reserveBeneficiaryOf; /// @notice Returns the ID of the first tier in the provided category on the provided 721 contract. /// @custom:param hook The 721 contract to get the category's first tier ID from. /// @custom:param category The category to get the first tier ID of. mapping(address hook => mapping(uint256 category => uint256)) internal _startingTierIdOfCategory; + /// @notice Returns the stored tier of the provided tier ID on the provided `IJB721TiersHook` contract. + /// @custom:param hook The address of the 721 contract to get the tier from. + /// @custom:param tierId The ID of the tier to get. + /// @custom:returns The stored tier, as a `JBStored721Tier` struct. + mapping(address hook => mapping(uint256 tierId => JBStored721Tier)) internal _storedTierOf; + + /// @notice Returns the ID of the tier which comes after the provided tier ID (sorted by price). + /// @dev If empty, assume the next tier ID should come after. + /// @custom:param hook The address of the 721 contract to get the next tier ID from. + /// @custom:param tierId The ID of the tier to get the next tier ID in relation to. + /// @custom:returns The following tier's ID. + mapping(address hook => mapping(uint256 tierId => uint256)) internal _tierIdAfter; + //*********************************************************************// // ------------------------- external views -------------------------- // //*********************************************************************// + /// @notice Resolves the encoded IPFS URI for the tier of the 721 with the provided token ID from the provided 721 + /// contract. + /// @param hook The 721 contract that the encoded IPFS URI belongs to. + /// @param tokenId The token ID of the 721 to get the encoded tier IPFS URI of. + /// @return The encoded IPFS URI. + function encodedTierIPFSUriOf(address hook, uint256 tokenId) external view override returns (bytes32) { + return encodedIPFSUriOf[hook][tierIdOfToken(tokenId)]; + } + + /// @notice Get the flags that dictate the behavior of the provided 721 contract. + /// @param hook The 721 contract to get the flags of. + /// @return The flags. + function flagsOf(address hook) external view override returns (JB721TiersHookFlags memory) { + return _flagsOf[hook]; + } + + /// @notice Check if the provided tier has been removed from the provided 721 contract. + /// @param hook The 721 contract the tier belongs to. + /// @param tierId The ID of the tier to check the removal status of. + /// @return A bool which is `true` if the tier has been removed, and `false` otherwise. + function isTierRemoved(address hook, uint256 tierId) external view override returns (bool) { + JBBitmapWord memory bitmapWord = _removedTiersBitmapWordOf[hook].readId(tierId); + + return bitmapWord.isTierIdRemoved(tierId); + } + + /// @notice Get the number of pending reserve NFTs for the provided tier ID of the provided 721 contract. + /// @dev "Pending" means that the NFTs have been reserved, but have not been minted yet. + /// @param hook The 721 contract to check for pending reserved NFTs. + /// @param tierId The ID of the tier to get the number of pending reserves for. + /// @return The number of pending reserved NFTs. + function numberOfPendingReservesFor(address hook, uint256 tierId) external view override returns (uint256) { + return _numberOfPendingReservesFor(hook, tierId, _storedTierOf[hook][tierId]); + } + + /// @notice Get the tier with the provided ID from the provided 721 contract. + /// @param hook The 721 contract to get the tier from. + /// @param id The ID of the tier to get. + /// @param includeResolvedUri If set to `true`, if the contract has a token URI resolver, its content will be + /// resolved and included. + /// @return The tier. + function tierOf( + address hook, + uint256 id, + bool includeResolvedUri + ) + public + view + override + returns (JB721Tier memory) + { + return _getTierFrom(hook, id, _storedTierOf[hook][id], includeResolvedUri); + } + + /// @notice Get the tier of the 721 with the provided token ID in the provided 721 contract. + /// @param hook The 721 contract that the tier belongs to. + /// @param tokenId The token ID of the 721 to get the tier of. + /// @param includeResolvedUri If set to `true`, if the contract has a token URI resolver, its content will be + /// resolved and included. + /// @return The tier. + function tierOfTokenId( + address hook, + uint256 tokenId, + bool includeResolvedUri + ) + external + view + override + returns (JB721Tier memory) + { + // Get a reference to the tier's ID. + uint256 tierId = tierIdOfToken(tokenId); + return _getTierFrom(hook, tierId, _storedTierOf[hook][tierId], includeResolvedUri); + } + + /// @notice Returns the number of voting units an addresses has within the specified tier of the specified 721 + /// contract. + /// @dev NFTs have a tier-specific number of voting units. If the tier does not have a custom number of voting + /// units, the price is used. + /// @param hook The 721 contract that the tier belongs to. + /// @param account The address to get the voting units of within the tier. + /// @param tierId The ID of the tier to get voting units within. + /// @return The address' voting units within the tier. + function tierVotingUnitsOf( + address hook, + address account, + uint256 tierId + ) + external + view + virtual + override + returns (uint256) + { + // Get a reference to the account's balance in this tier. + uint256 balance = tierBalanceOf[hook][account][tierId]; + + if (balance == 0) return 0; + + // Keep a reference to the stored tier. + JBStored721Tier memory storedTier = _storedTierOf[hook][tierId]; + + // Check if voting units should be used. Price will be used otherwise. + (,, bool useVotingUnits,,) = _unpackBools(storedTier.packedBools); + + // Return the address' voting units within the tier. + return balance * (useVotingUnits ? storedTier.votingUnits : storedTier.price); + } + /// @notice Gets an array of currently active 721 tiers for the provided 721 contract. /// @param hook The 721 contract to get the tiers of. /// @param categories An array tier categories to get tiers from. Send an empty array to get all categories. @@ -223,46 +337,6 @@ contract JB721TiersHookStore is IJB721TiersHookStore { } } - /// @notice Get the tier with the provided ID from the provided 721 contract. - /// @param hook The 721 contract to get the tier from. - /// @param id The ID of the tier to get. - /// @param includeResolvedUri If set to `true`, if the contract has a token URI resolver, its content will be - /// resolved and included. - /// @return The tier. - function tierOf( - address hook, - uint256 id, - bool includeResolvedUri - ) - public - view - override - returns (JB721Tier memory) - { - return _getTierFrom(hook, id, _storedTierOf[hook][id], includeResolvedUri); - } - - /// @notice Get the tier of the 721 with the provided token ID in the provided 721 contract. - /// @param hook The 721 contract that the tier belongs to. - /// @param tokenId The token ID of the 721 to get the tier of. - /// @param includeResolvedUri If set to `true`, if the contract has a token URI resolver, its content will be - /// resolved and included. - /// @return tier The tier. - function tierOfTokenId( - address hook, - uint256 tokenId, - bool includeResolvedUri - ) - external - view - override - returns (JB721Tier memory) - { - // Get a reference to the tier's ID. - uint256 tierId = tierIdOfToken(tokenId); - return _getTierFrom(hook, tierId, _storedTierOf[hook][tierId], includeResolvedUri); - } - /// @notice Get the number of NFTs which have been minted from the provided 721 contract (across all tiers). /// @param hook The 721 contract to get a total supply of. /// @return supply The total number of NFTs minted from all tiers on the contract. @@ -282,15 +356,6 @@ contract JB721TiersHookStore is IJB721TiersHookStore { } } - /// @notice Get the number of pending reserve NFTs for the provided tier ID of the provided 721 contract. - /// @dev "Pending" means that the NFTs have been reserved, but have not been minted yet. - /// @param hook The 721 contract to check for pending reserved NFTs. - /// @param tierId The ID of the tier to get the number of pending reserves for. - /// @return The number of pending reserved NFTs. - function numberOfPendingReservesFor(address hook, uint256 tierId) external view override returns (uint256) { - return _numberOfPendingReservesFor(hook, tierId, _storedTierOf[hook][tierId]); - } - /// @notice Get the number of voting units the provided address has for the provided 721 contract (across all /// tiers). /// @dev NFTs have a tier-specific number of voting units. If the tier does not have a custom number of voting @@ -320,7 +385,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore { storedTier = _storedTierOf[hook][i]; // Parse the flags. - (,, bool useVotingUnits,) = _unpackBools(storedTier.packedBools); + (,, bool useVotingUnits,,) = _unpackBools(storedTier.packedBools); // Add the voting units for the address' balance in this tier. // Use custom voting units if set. Otherwise, use the tier's price. @@ -328,66 +393,6 @@ contract JB721TiersHookStore is IJB721TiersHookStore { } } - /// @notice Returns the number of voting units an addresses has within the specified tier of the specified 721 - /// contract. - /// @dev NFTs have a tier-specific number of voting units. If the tier does not have a custom number of voting - /// units, the price is used. - /// @param hook The 721 contract that the tier belongs to. - /// @param account The address to get the voting units of within the tier. - /// @param tierId The ID of the tier to get voting units within. - /// @return The address' voting units within the tier. - function tierVotingUnitsOf( - address hook, - address account, - uint256 tierId - ) - external - view - virtual - override - returns (uint256) - { - // Get a reference to the account's balance in this tier. - uint256 balance = tierBalanceOf[hook][account][tierId]; - - if (balance == 0) return 0; - - // Keep a reference to the stored tier. - JBStored721Tier memory storedTier = _storedTierOf[hook][tierId]; - - // Check if voting units should be used. Price will be used otherwise. - (,, bool useVotingUnits,) = _unpackBools(storedTier.packedBools); - - // Return the address' voting units within the tier. - return balance * (useVotingUnits ? storedTier.votingUnits : storedTier.price); - } - - /// @notice Resolves the encoded IPFS URI for the tier of the 721 with the provided token ID from the provided 721 - /// contract. - /// @param hook The 721 contract that the encoded IPFS URI belongs to. - /// @param tokenId The token ID of the 721 to get the encoded tier IPFS URI of. - /// @return The encoded IPFS URI. - function encodedTierIPFSUriOf(address hook, uint256 tokenId) external view override returns (bytes32) { - return encodedIPFSUriOf[hook][tierIdOfToken(tokenId)]; - } - - /// @notice Get the flags that dictate the behavior of the provided 721 contract. - /// @param hook The 721 contract to get the flags of. - /// @return The flags. - function flagsOf(address hook) external view override returns (JB721TiersHookFlags memory) { - return _flagsOf[hook]; - } - - /// @notice Check if the provided tier has been removed from the provided 721 contract. - /// @param hook The 721 contract the tier belongs to. - /// @param tierId The ID of the tier to check the removal status of. - /// @return A bool which is `true` if the tier has been removed, and `false` otherwise. - function isTierRemoved(address hook, uint256 tierId) external view override returns (bool) { - JBBitmapWord memory bitmapWord = _removedTiersBitmapWordOf[hook].readId(tierId); - - return bitmapWord.isTierIdRemoved(tierId); - } - //*********************************************************************// // -------------------------- public views --------------------------- // //*********************************************************************// @@ -433,6 +438,31 @@ contract JB721TiersHookStore is IJB721TiersHookStore { } } + /// @notice The reserve beneficiary for the provided tier ID on the provided 721 contract. + /// @param hook The 721 contract that the tier belongs to. + /// @param tierId The ID of the tier to get the reserve beneficiary of. + /// @return The reserve beneficiary for the tier. + function reserveBeneficiaryOf(address hook, uint256 tierId) public view override returns (address) { + // Get the stored reserve beneficiary. + address storedReserveBeneficiaryOfTier = _reserveBeneficiaryOf[hook][tierId]; + + // If the tier has a beneficiary specified, return it. + if (storedReserveBeneficiaryOfTier != address(0)) { + return storedReserveBeneficiaryOfTier; + } + + // Otherwise, return the contract's default reserve benficiary. + return defaultReserveBeneficiaryOf[hook]; + } + + /// @notice The tier ID for the 721 with the provided token ID. + /// @dev Tiers are 1-indexed from the `tiers` array, meaning the 0th element of the array is tier 1. + /// @param tokenId The token ID of the 721 to get the tier ID of. + /// @return The ID of the 721's tier. + function tierIdOfToken(uint256 tokenId) public pure override returns (uint256) { + return tokenId / _ONE_BILLION; + } + /// @notice The combined redemption weight for all NFTs from the provided 721 contract. /// @param hook The 721 contract to get the total redemption weight of. /// @return weight The total redemption weight. @@ -459,39 +489,57 @@ contract JB721TiersHookStore is IJB721TiersHookStore { } } - /// @notice The tier ID for the 721 with the provided token ID. - /// @dev Tiers are 1-indexed from the `tiers` array, meaning the 0th element of the array is tier 1. - /// @param tokenId The token ID of the 721 to get the tier ID of. - /// @return The ID of the 721's tier. - function tierIdOfToken(uint256 tokenId) public pure override returns (uint256) { - return tokenId / _ONE_BILLION; - } + //*********************************************************************// + // ---------------------- external transactions ---------------------- // + //*********************************************************************// - /// @notice The reserve beneficiary for the provided tier ID on the provided 721 contract. - /// @param hook The 721 contract that the tier belongs to. - /// @param tierId The ID of the tier to get the reserve beneficiary of. - /// @return The reserve beneficiary for the tier. - function reserveBeneficiaryOf(address hook, uint256 tierId) public view override returns (address) { - // Get the stored reserve beneficiary. - address storedReserveBeneficiaryOfTier = _reserveBeneficiaryOf[hook][tierId]; + /// @notice Cleans an 721 contract's removed tiers from the tier sorting sequence. + /// @param hook The 721 contract to clean tiers for. + function cleanTiers(address hook) external override { + // Keep a reference to the last tier ID. + uint256 lastSortedTierId = _lastSortedTierIdOf(hook); - // If the tier has a beneficiary specified, return it. - if (storedReserveBeneficiaryOfTier != address(0)) { - return storedReserveBeneficiaryOfTier; + // Get a reference to the tier ID being iterated on, starting with the starting tier ID. + uint256 currentSortedTierId = _firstSortedTierIdOf(hook, 0); + + // Keep track of the previous non-removed tier ID. + uint256 previousSortedTierId; + + // Initialize a `JBBitmapWord` for tracking removed tiers. + JBBitmapWord memory bitmapWord; + + // Make the sorted array. + while (currentSortedTierId != 0) { + // If the current tier ID being iterated on isn't an increment of the previous one, + if (!_isTierRemovedWithRefresh(hook, currentSortedTierId, bitmapWord)) { + // Update its `_tierIdAfter` if needed. + if (currentSortedTierId != previousSortedTierId + 1) { + if (_tierIdAfter[hook][previousSortedTierId] != currentSortedTierId) { + _tierIdAfter[hook][previousSortedTierId] = currentSortedTierId; + } + // Otherwise, if the current tier ID IS an increment of the previous one, + // AND the tier ID after it isn't 0, + } else if (_tierIdAfter[hook][previousSortedTierId] != 0) { + // Set its `_tierIdAfter` to 0. + _tierIdAfter[hook][previousSortedTierId] = 0; + } + + // Iterate by setting the previous tier ID for the next loop to the current tier ID. + previousSortedTierId = currentSortedTierId; + } + // Iterate by updating the current sorted tier ID to the next sorted tier ID. + currentSortedTierId = _nextSortedTierIdOf(hook, currentSortedTierId, lastSortedTierId); } - // Otherwise, return the contract's default reserve benficiary. - return defaultReserveBeneficiaryOf[hook]; + emit CleanTiers({hook: hook, caller: msg.sender}); } - //*********************************************************************// - // ---------------------- external transactions ---------------------- // - //*********************************************************************// - /// @notice Record newly added tiers. /// @param tiersToAdd The tiers to add. /// @return tierIds The IDs of the tiers being added. - function recordAddTiers(JB721TierConfig[] calldata tiersToAdd) + function recordAddTiers( + JB721TierConfig[] calldata tiersToAdd + ) external override returns (uint256[] memory tierIds) @@ -503,7 +551,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore { uint256 currentMaxTierIdOf = maxTierIdOf[msg.sender]; // Make sure the max number of tiers won't be exceeded. - if (currentMaxTierIdOf + numberOfNewTiers > type(uint16).max) revert MAX_TIERS_EXCEEDED(); + if (currentMaxTierIdOf + numberOfNewTiers > type(uint16).max) revert JB721TiersHookStore_MaxTiersExceeded(); // Keep a reference to the current last sorted tier ID (sorted by price). uint256 currentLastSortedTierId = _lastSortedTierIdOf(msg.sender); @@ -530,7 +578,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore { // Make sure the supply maximum is enforced. If it's greater than one billion, it would overflow into the // next tier. - if (tierToAdd.initialSupply > _ONE_BILLION - 1) revert INVALID_QUANTITY(); + if (tierToAdd.initialSupply > _ONE_BILLION - 1) revert JB721TiersHookStore_InvalidQuantity(); // Keep a reference to the previous tier. JB721TierConfig memory previousTier; @@ -541,7 +589,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore { previousTier = tiersToAdd[i - 1]; // Revert if the category is not equal or greater than the previously added tier's category. - if (tierToAdd.category < previousTier.category) revert INVALID_CATEGORY_SORT_ORDER(); + if (tierToAdd.category < previousTier.category) revert JB721TiersHookStore_InvalidCategorySortOrder(); } // Make sure the new tier doesn't have voting units if the 721 contract's flags don't allow it to. @@ -552,22 +600,27 @@ contract JB721TiersHookStore is IJB721TiersHookStore { || (!tierToAdd.useVotingUnits && tierToAdd.price != 0) ) ) { - revert VOTING_UNITS_NOT_ALLOWED(); + revert JB721TiersHookStore_VotingUnitsNotAllowed(); } // Make sure the new tier doesn't have a reserve frequency if the 721 contract's flags don't allow it to, // OR if manual minting is allowed. if ((flags.noNewTiersWithReserves || tierToAdd.allowOwnerMint) && tierToAdd.reserveFrequency != 0) { - revert RESERVE_FREQUENCY_NOT_ALLOWED(); + revert JB721TiersHookStore_ReserveFrequencyNotAllowed(); } // Make sure the new tier doesn't have owner minting enabled if the 721 contract's flags don't allow it to. if (flags.noNewTiersWithOwnerMinting && tierToAdd.allowOwnerMint) { - revert MANUAL_MINTING_NOT_ALLOWED(); + revert JB721TiersHookStore_ManualMintingNotAllowed(); + } + + // Make sure the discount percent is within the bound. + if (tierToAdd.discountPercent > JB721Constants.MAX_DISCOUNT_PERCENT) { + revert JB721TiersHookStore_DiscountPercentExceedsBounds(); } // Make sure the tier has a non-zero supply. - if (tierToAdd.initialSupply == 0) revert NO_SUPPLY(); + if (tierToAdd.initialSupply == 0) revert JB721TiersHookStore_NoSupply(); // Get a reference to the ID for the new tier. uint256 tierId = currentMaxTierIdOf + i + 1; @@ -577,14 +630,16 @@ contract JB721TiersHookStore is IJB721TiersHookStore { price: uint104(tierToAdd.price), remainingSupply: uint32(tierToAdd.initialSupply), initialSupply: uint32(tierToAdd.initialSupply), - votingUnits: uint40(tierToAdd.votingUnits), + votingUnits: uint32(tierToAdd.votingUnits), reserveFrequency: uint16(tierToAdd.reserveFrequency), category: uint24(tierToAdd.category), + discountPercent: uint8(tierToAdd.discountPercent), packedBools: _packBools( tierToAdd.allowOwnerMint, tierToAdd.transfersPausable, tierToAdd.useVotingUnits, - tierToAdd.cannotBeRemoved + tierToAdd.cannotBeRemoved, + tierToAdd.cannotIncreaseDiscountPercent ) }); @@ -692,92 +747,36 @@ contract JB721TiersHookStore is IJB721TiersHookStore { maxTierIdOf[msg.sender] = currentMaxTierIdOf + numberOfNewTiers; } - /// @notice Record reserve 721 minting for the provided tier ID on the provided 721 contract. - /// @param tierId The ID of the tier to mint reserves from. - /// @param count The number of reserve NFTs to mint. - /// @return tokenIds The token IDs of the reserve NFTs which were minted. - function recordMintReservesFor( - uint256 tierId, - uint256 count - ) - external - override - returns (uint256[] memory tokenIds) - { - // Get a reference to the stored tier. - JBStored721Tier storage storedTier = _storedTierOf[msg.sender][tierId]; - - // Get a reference to the number of pending reserve NFTs for the tier. - // "Pending" means that the NFTs have been reserved, but have not been minted yet. - uint256 numberOfPendingReserves = _numberOfPendingReservesFor(msg.sender, tierId, storedTier); - - // Can't mint more than the number of pending reserves. - if (count > numberOfPendingReserves) revert INSUFFICIENT_PENDING_RESERVES(); - - // Increment the number of reserve NFTs minted. - numberOfReservesMintedFor[msg.sender][tierId] += count; - - // Initialize an array for the token IDs to be returned. - tokenIds = new uint256[](count); - - // Keep a reference to the number of NFTs burned within the tier. - uint256 numberOfBurnedFromTier = numberOfBurnedFor[msg.sender][tierId]; - - for (uint256 i; i < count; i++) { - // Generate the NFTs. - tokenIds[i] = _generateTokenId( - tierId, storedTier.initialSupply - --storedTier.remainingSupply + numberOfBurnedFromTier - ); - } - } - - /// @notice Record an 721 transfer. - /// @param tierId The ID of the tier that the 721 being transferred belongs to. - /// @param from The address that the 721 is being transferred from. - /// @param to The address that the 721 is being transferred to. - function recordTransferForTier(uint256 tierId, address from, address to) external override { - // If this is not a mint, - if (from != address(0)) { - // then subtract the tier balance from the sender. - --tierBalanceOf[msg.sender][from][tierId]; - } - - // If this is not a burn, - if (to != address(0)) { - unchecked { - // then increase the tier balance for the receiver. - ++tierBalanceOf[msg.sender][to][tierId]; - } - } - } - - /// @notice Record tiers being removed. - /// @param tierIds The IDs of the tiers being removed. - function recordRemoveTierIds(uint256[] calldata tierIds) external override { - // Get a reference to the number of tiers being removed. - uint256 numTiers = tierIds.length; - - // Keep a reference to the tier ID being iterated upon. - uint256 tierId; + /// @notice Records 721 burns. + /// @param tokenIds The token IDs of the NFTs to burn. + function recordBurn(uint256[] calldata tokenIds) external override { + // Get a reference to the number of token IDs provided. + uint256 numberOfTokenIds = tokenIds.length; - for (uint256 i; i < numTiers; i++) { - // Set the tier being iterated upon (0-indexed). - tierId = tierIds[i]; + // Keep a reference to the token ID being iterated on. + uint256 tokenId; - // Get a reference to the stored tier. - JBStored721Tier storage storedTier = _storedTierOf[msg.sender][tierId]; + // Iterate through all token IDs to increment the burn count. + for (uint256 i; i < numberOfTokenIds; i++) { + // Set the 721's token ID. + tokenId = tokenIds[i]; - // Parse the flags. - (,,, bool cannotBeRemoved) = _unpackBools(storedTier.packedBools); + uint256 tierId = tierIdOfToken(tokenId); - // Make sure the tier can be removed. - if (cannotBeRemoved) revert CANT_REMOVE_TIER(); + // Increment the number of NFTs burned from the tier. + numberOfBurnedFor[msg.sender][tierId]++; - // Remove the tier by marking it as removed in the bitmap. - _removedTiersBitmapWordOf[msg.sender].removeTier(tierId); + // Increment the remaining supply of the tier. + _storedTierOf[msg.sender][tierId].remainingSupply++; } } + /// @notice Record newly set flags. + /// @param flags The flags to set. + function recordFlags(JB721TiersHookFlags calldata flags) external override { + _flagsOf[msg.sender] = flags; + } + /// @notice Record 721 mints from the provided tiers. /// @param amount The amount being spent on NFTs. The total price must not exceed this amount. /// @param tierIds The IDs of the tiers to mint from. @@ -816,26 +815,34 @@ contract JB721TiersHookStore is IJB721TiersHookStore { tierId = tierIds[i]; // Make sure the tier hasn't been removed. - if (_isTierRemovedWithRefresh(msg.sender, tierId, bitmapWord)) revert TIER_REMOVED(); + if (_isTierRemovedWithRefresh(msg.sender, tierId, bitmapWord)) revert JB721TiersHookStore_TierRemoved(); // Keep a reference to the stored tier being iterated on. storedTier = _storedTierOf[msg.sender][tierId]; // Parse the flags. - (bool allowOwnerMint,,,) = _unpackBools(storedTier.packedBools); + (bool allowOwnerMint,,,,) = _unpackBools(storedTier.packedBools); // If this is an owner mint, make sure owner minting is allowed. - if (isOwnerMint && !allowOwnerMint) revert CANT_MINT_MANUALLY(); + if (isOwnerMint && !allowOwnerMint) revert JB721TiersHookStore_CantMintManually(); // Make sure the provided tier exists (tiers cannot have a supply of 0). - if (storedTier.initialSupply == 0) revert INVALID_TIER(); + if (storedTier.initialSupply == 0) revert JB721TiersHookStore_InvalidTier(); + + // Get a reference to the price. + uint256 price = storedTier.price; + + // Apply a discount if needed. + if (storedTier.discountPercent > 0) { + price -= mulDiv(price, storedTier.discountPercent, JB721Constants.MAX_DISCOUNT_PERCENT); + } // Make sure the `amount` is greater than or equal to the tier's price. - if (storedTier.price > leftoverAmount) revert PRICE_EXCEEDS_AMOUNT(); + if (price > leftoverAmount) revert JB721TiersHookStore_PriceExceedsAmount(); // Make sure there are enough NFTs available to mint. if (storedTier.remainingSupply <= _numberOfPendingReservesFor(msg.sender, tierId, storedTier)) { - revert INSUFFICIENT_SUPPLY_REMAINING(); + revert JB721TiersHookStore_InsufficientSupplyRemaining(); } // Mint the 721. @@ -845,39 +852,99 @@ contract JB721TiersHookStore is IJB721TiersHookStore { tierId, storedTier.initialSupply - --storedTier.remainingSupply + numberOfBurnedFor[msg.sender][tierId] ); - leftoverAmount = leftoverAmount - storedTier.price; + leftoverAmount = leftoverAmount - price; } } } - /// @notice Records 721 burns. - /// @param tokenIds The token IDs of the NFTs to burn. - function recordBurn(uint256[] calldata tokenIds) external override { - // Get a reference to the number of token IDs provided. - uint256 numberOfTokenIds = tokenIds.length; + /// @notice Record reserve 721 minting for the provided tier ID on the provided 721 contract. + /// @param tierId The ID of the tier to mint reserves from. + /// @param count The number of reserve NFTs to mint. + /// @return tokenIds The token IDs of the reserve NFTs which were minted. + function recordMintReservesFor( + uint256 tierId, + uint256 count + ) + external + override + returns (uint256[] memory tokenIds) + { + // Get a reference to the stored tier. + JBStored721Tier storage storedTier = _storedTierOf[msg.sender][tierId]; - // Keep a reference to the token ID being iterated on. - uint256 tokenId; + // Get a reference to the number of pending reserve NFTs for the tier. + // "Pending" means that the NFTs have been reserved, but have not been minted yet. + uint256 numberOfPendingReserves = _numberOfPendingReservesFor(msg.sender, tierId, storedTier); - // Iterate through all token IDs to increment the burn count. - for (uint256 i; i < numberOfTokenIds; i++) { - // Set the 721's token ID. - tokenId = tokenIds[i]; + // Can't mint more than the number of pending reserves. + if (count > numberOfPendingReserves) revert JB721TiersHookStore_InsufficientPendingReserves(); - uint256 tierId = tierIdOfToken(tokenId); + // Increment the number of reserve NFTs minted. + numberOfReservesMintedFor[msg.sender][tierId] += count; - // Increment the number of NFTs burned from the tier. - numberOfBurnedFor[msg.sender][tierId]++; + // Initialize an array for the token IDs to be returned. + tokenIds = new uint256[](count); - // Increment the remaining supply of the tier. - _storedTierOf[msg.sender][tierId].remainingSupply++; + // Keep a reference to the number of NFTs burned within the tier. + uint256 numberOfBurnedFromTier = numberOfBurnedFor[msg.sender][tierId]; + + for (uint256 i; i < count; i++) { + // Generate the NFTs. + tokenIds[i] = _generateTokenId( + tierId, storedTier.initialSupply - --storedTier.remainingSupply + numberOfBurnedFromTier + ); } } - /// @notice Record a newly set token URI resolver. - /// @param resolver The resolver to set. - function recordSetTokenUriResolver(IJB721TokenUriResolver resolver) external override { - tokenUriResolverOf[msg.sender] = resolver; + /// @notice Record tiers being removed. + /// @param tierIds The IDs of the tiers being removed. + function recordRemoveTierIds(uint256[] calldata tierIds) external override { + // Get a reference to the number of tiers being removed. + uint256 numTiers = tierIds.length; + + // Keep a reference to the tier ID being iterated upon. + uint256 tierId; + + for (uint256 i; i < numTiers; i++) { + // Set the tier being iterated upon (0-indexed). + tierId = tierIds[i]; + + // Get a reference to the stored tier. + JBStored721Tier storage storedTier = _storedTierOf[msg.sender][tierId]; + + // Parse the flags. + (,,, bool cannotBeRemoved,) = _unpackBools(storedTier.packedBools); + + // Make sure the tier can be removed. + if (cannotBeRemoved) revert JB721TiersHookStore_CantRemoveTier(); + + // Remove the tier by marking it as removed in the bitmap. + _removedTiersBitmapWordOf[msg.sender].removeTier(tierId); + } + } + + /// @notice Records the setting of a discount for a tier. + /// @param tierId The ID of the tier to record a discount for. + /// @param discountPercent The new discount percent being applied. + function recordSetDiscountPercentOf(uint256 tierId, uint256 discountPercent) external override { + // Make sure the discount percent is within the bound. + if (discountPercent > JB721Constants.MAX_DISCOUNT_PERCENT) { + revert JB721TiersHookStore_DiscountPercentExceedsBounds(); + } + + // Get a reference to the stored tier. + JBStored721Tier storage storedTier = _storedTierOf[msg.sender][tierId]; + + // Parse the flags. + (,,,, bool cannotIncreaseDiscountPercent) = _unpackBools(storedTier.packedBools); + + // Make sure that increasing the discount is allowed for the tier. + if (discountPercent > storedTier.discountPercent && cannotIncreaseDiscountPercent) { + revert JB721TiersHookStore_DiscountPercentIncreaseNotAllowed(); + } + + // Set the discount. + storedTier.discountPercent = uint8(discountPercent); } /// @notice Record a new encoded IPFS URI for a tier. @@ -887,57 +954,55 @@ contract JB721TiersHookStore is IJB721TiersHookStore { encodedIPFSUriOf[msg.sender][tierId] = encodedIPFSUri; } - /// @notice Record newly set flags. - /// @param flags The flags to set. - function recordFlags(JB721TiersHookFlags calldata flags) external override { - _flagsOf[msg.sender] = flags; + /// @notice Record a newly set token URI resolver. + /// @param resolver The resolver to set. + function recordSetTokenUriResolver(IJB721TokenUriResolver resolver) external override { + tokenUriResolverOf[msg.sender] = resolver; } - /// @notice Cleans an 721 contract's removed tiers from the tier sorting sequence. - /// @param hook The 721 contract to clean tiers for. - function cleanTiers(address hook) external override { - // Keep a reference to the last tier ID. - uint256 lastSortedTierId = _lastSortedTierIdOf(hook); - - // Get a reference to the tier ID being iterated on, starting with the starting tier ID. - uint256 currentSortedTierId = _firstSortedTierIdOf(hook, 0); - - // Keep track of the previous non-removed tier ID. - uint256 previousSortedTierId; - - // Initialize a `JBBitmapWord` for tracking removed tiers. - JBBitmapWord memory bitmapWord; - - // Make the sorted array. - while (currentSortedTierId != 0) { - // If the current tier ID being iterated on isn't an increment of the previous one, - if (!_isTierRemovedWithRefresh(hook, currentSortedTierId, bitmapWord)) { - // Update its `_tierIdAfter` if needed. - if (currentSortedTierId != previousSortedTierId + 1) { - if (_tierIdAfter[hook][previousSortedTierId] != currentSortedTierId) { - _tierIdAfter[hook][previousSortedTierId] = currentSortedTierId; - } - // Otherwise, if the current tier ID IS an increment of the previous one, - // AND the tier ID after it isn't 0, - } else if (_tierIdAfter[hook][previousSortedTierId] != 0) { - // Set its `_tierIdAfter` to 0. - _tierIdAfter[hook][previousSortedTierId] = 0; - } + /// @notice Record an 721 transfer. + /// @param tierId The ID of the tier that the 721 being transferred belongs to. + /// @param from The address that the 721 is being transferred from. + /// @param to The address that the 721 is being transferred to. + function recordTransferForTier(uint256 tierId, address from, address to) external override { + // If this is not a mint, + if (from != address(0)) { + // then subtract the tier balance from the sender. + --tierBalanceOf[msg.sender][from][tierId]; + } - // Iterate by setting the previous tier ID for the next loop to the current tier ID. - previousSortedTierId = currentSortedTierId; + // If this is not a burn, + if (to != address(0)) { + unchecked { + // then increase the tier balance for the receiver. + ++tierBalanceOf[msg.sender][to][tierId]; } - // Iterate by updating the current sorted tier ID to the next sorted tier ID. - currentSortedTierId = _nextSortedTierIdOf(hook, currentSortedTierId, lastSortedTierId); } - - emit CleanTiers(hook, msg.sender); } //*********************************************************************// // ------------------------ internal functions ----------------------- // //*********************************************************************// + /// @notice Get the first tier ID from an 721 contract (when sorted by price) within a provided category. + /// @param hook The 721 contract to get the first sorted tier ID of. + /// @param category The category to get the first sorted tier ID within. Send 0 for the first ID across all tiers, + /// which might not be in the 0th category if the 0th category does not exist. + /// @return id The first sorted tier ID within the provided category. + function _firstSortedTierIdOf(address hook, uint256 category) internal view returns (uint256 id) { + id = category == 0 ? _tierIdAfter[hook][0] : _startingTierIdOfCategory[hook][category]; + // Start at the first tier ID if nothing is specified. + if (id == 0) id = 1; + } + + /// @notice Generate a token ID for an 721 given a tier ID and a token number within that tier. + /// @param tierId The ID of the tier to generate a token ID for. + /// @param tokenNumber The token number of the 721 within the tier. + /// @return The token ID of the 721. + function _generateTokenId(uint256 tierId, uint256 tokenNumber) internal pure returns (uint256) { + return (tierId * _ONE_BILLION) + tokenNumber; + } + /// @notice Returns the tier corresponding to the stored tier provided. /// @dev Translate `JBStored721Tier` to `JB721Tier`. /// @param hook The 721 contract to get the tier from. @@ -959,8 +1024,13 @@ contract JB721TiersHookStore is IJB721TiersHookStore { // Get a reference to the reserve beneficiary. address reserveBeneficiary = reserveBeneficiaryOf(hook, tierId); - (bool allowOwnerMint, bool transfersPausable, bool useVotingUnits, bool cannotBeRemoved) = - _unpackBools(storedTier.packedBools); + ( + bool allowOwnerMint, + bool transfersPausable, + bool useVotingUnits, + bool cannotBeRemoved, + bool cannotIncreaseDiscountPercent + ) = _unpackBools(storedTier.packedBools); // slither-disable-next-line calls-loop return JB721Tier({ @@ -974,9 +1044,11 @@ contract JB721TiersHookStore is IJB721TiersHookStore { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: encodedIPFSUriOf[hook][tierId], category: storedTier.category, + discountPercent: storedTier.discountPercent, allowOwnerMint: allowOwnerMint, transfersPausable: transfersPausable, cannotBeRemoved: cannotBeRemoved, + cannotIncreaseDiscountPercent: cannotIncreaseDiscountPercent, resolvedUri: !includeResolvedUri || tokenUriResolverOf[hook] == IJB721TokenUriResolver(address(0)) ? "" : tokenUriResolverOf[hook].tokenUriOf(hook, _generateTokenId(tierId, 0)) @@ -1005,6 +1077,33 @@ contract JB721TiersHookStore is IJB721TiersHookStore { return bitmapWord.isTierIdRemoved(tierId); } + /// @notice The last sorted tier ID from an 721 contract (when sorted by price). + /// @param hook The 721 contract to get the last sorted tier ID of. + /// @return id The last sorted tier ID. + function _lastSortedTierIdOf(address hook) internal view returns (uint256 id) { + id = _lastTrackedSortedTierIdOf[hook]; + // Use the maximum tier ID if nothing is specified. + if (id == 0) id = maxTierIdOf[hook]; + } + + /// @notice Get the tier ID which comes after the provided one when sorted by price. + /// @param hook The 721 contract to get the next sorted tier ID from. + /// @param id The tier ID to get the next sorted tier ID relative to. + /// @param max The maximum tier ID. + /// @return The next sorted tier ID. + function _nextSortedTierIdOf(address hook, uint256 id, uint256 max) internal view returns (uint256) { + // If this is the last tier (maximum), return zero. + if (id == max) return 0; + + // If a tier ID is saved to come after the provided ID, return it. + uint256 storedNext = _tierIdAfter[hook][id]; + + if (storedNext != 0) return storedNext; + + // Otherwise, increment the provided tier ID. + return id + 1; + } + /// @notice Get the number of pending reserve NFTs for the specified tier ID. /// @param hook The 721 contract that the tier belongs to. /// @param tierId The ID of the tier to get the number of pending reserve NFTs for. @@ -1064,63 +1163,19 @@ contract JB721TiersHookStore is IJB721TiersHookStore { } } - /// @notice Generate a token ID for an 721 given a tier ID and a token number within that tier. - /// @param tierId The ID of the tier to generate a token ID for. - /// @param tokenNumber The token number of the 721 within the tier. - /// @return The token ID of the 721. - function _generateTokenId(uint256 tierId, uint256 tokenNumber) internal pure returns (uint256) { - return (tierId * _ONE_BILLION) + tokenNumber; - } - - /// @notice Get the tier ID which comes after the provided one when sorted by price. - /// @param hook The 721 contract to get the next sorted tier ID from. - /// @param id The tier ID to get the next sorted tier ID relative to. - /// @param max The maximum tier ID. - /// @return The next sorted tier ID. - function _nextSortedTierIdOf(address hook, uint256 id, uint256 max) internal view returns (uint256) { - // If this is the last tier (maximum), return zero. - if (id == max) return 0; - - // If a tier ID is saved to come after the provided ID, return it. - uint256 storedNext = _tierIdAfter[hook][id]; - - if (storedNext != 0) return storedNext; - - // Otherwise, increment the provided tier ID. - return id + 1; - } - - /// @notice Get the first tier ID from an 721 contract (when sorted by price) within a provided category. - /// @param hook The 721 contract to get the first sorted tier ID of. - /// @param category The category to get the first sorted tier ID within. Send 0 for the first ID across all tiers, - /// which might not be in the 0th category if the 0th category does not exist. - /// @return id The first sorted tier ID within the provided category. - function _firstSortedTierIdOf(address hook, uint256 category) internal view returns (uint256 id) { - id = category == 0 ? _tierIdAfter[hook][0] : _startingTierIdOfCategory[hook][category]; - // Start at the first tier ID if nothing is specified. - if (id == 0) id = 1; - } - - /// @notice The last sorted tier ID from an 721 contract (when sorted by price). - /// @param hook The 721 contract to get the last sorted tier ID of. - /// @return id The last sorted tier ID. - function _lastSortedTierIdOf(address hook) internal view returns (uint256 id) { - id = _lastTrackedSortedTierIdOf[hook]; - // Use the maximum tier ID if nothing is specified. - if (id == 0) id = maxTierIdOf[hook]; - } - /// @notice Pack three bools into a single uint8. /// @param allowOwnerMint Whether or not owner minting is allowed in new tiers. /// @param transfersPausable Whether or not 721 transfers can be paused. /// @param useVotingUnits Whether or not custom voting unit amounts are allowed in new tiers. /// @param cannotBeRemoved Whether or not attempts to remove the tier will revert. + /// @param cannotIncreaseDiscountPercent Whether or not attempts to increase the discount percent will revert. /// @return packed The packed bools. function _packBools( bool allowOwnerMint, bool transfersPausable, bool useVotingUnits, - bool cannotBeRemoved + bool cannotBeRemoved, + bool cannotIncreaseDiscountPercent ) internal pure @@ -1131,6 +1186,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore { packed := or(shl(0x1, transfersPausable), packed) packed := or(shl(0x2, useVotingUnits), packed) packed := or(shl(0x3, cannotBeRemoved), packed) + packed := or(shl(0x4, cannotIncreaseDiscountPercent), packed) } } @@ -1140,16 +1196,26 @@ contract JB721TiersHookStore is IJB721TiersHookStore { /// @param transfersPausable Whether or not 721 transfers can be paused. /// @param useVotingUnits Whether or not custom voting unit amounts are allowed in new tiers. /// @param cannotBeRemoved Whether or not the tier can be removed once added. - function _unpackBools(uint8 packed) + /// @param cannotIncreaseDiscountPercent Whether or not the discount percent cannot be increased. + function _unpackBools( + uint8 packed + ) internal pure - returns (bool allowOwnerMint, bool transfersPausable, bool useVotingUnits, bool cannotBeRemoved) + returns ( + bool allowOwnerMint, + bool transfersPausable, + bool useVotingUnits, + bool cannotBeRemoved, + bool cannotIncreaseDiscountPercent + ) { assembly { allowOwnerMint := iszero(iszero(and(0x1, packed))) transfersPausable := iszero(iszero(and(0x2, packed))) useVotingUnits := iszero(iszero(and(0x4, packed))) cannotBeRemoved := iszero(iszero(and(0x8, packed))) + cannotIncreaseDiscountPercent := iszero(iszero(and(0x10, packed))) } } } diff --git a/src/abstract/ERC721.sol b/src/abstract/ERC721.sol index 9b4e94c9..75ef5ba4 100644 --- a/src/abstract/ERC721.sol +++ b/src/abstract/ERC721.sol @@ -8,7 +8,8 @@ import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Recei import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; import {Context} from "@openzeppelin/contracts/utils/Context.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; -import {IERC165, ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {IERC721Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; /** diff --git a/src/abstract/JB721Hook.sol b/src/abstract/JB721Hook.sol index ff02b3ad..1bf5bd85 100644 --- a/src/abstract/JB721Hook.sol +++ b/src/abstract/JB721Hook.sol @@ -1,22 +1,22 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {mulDiv} from "@prb/math/src/Common.sol"; -import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol"; -import {IJBRulesetDataHook} from "@bananapus/core/src/interfaces/IJBRulesetDataHook.sol"; import {IJBDirectory} from "@bananapus/core/src/interfaces/IJBDirectory.sol"; import {IJBPayHook} from "@bananapus/core/src/interfaces/IJBPayHook.sol"; import {IJBRedeemHook} from "@bananapus/core/src/interfaces/IJBRedeemHook.sol"; +import {IJBRulesetDataHook} from "@bananapus/core/src/interfaces/IJBRulesetDataHook.sol"; import {IJBTerminal} from "@bananapus/core/src/interfaces/IJBTerminal.sol"; import {JBConstants} from "@bananapus/core/src/libraries/JBConstants.sol"; -import {JBBeforePayRecordedContext} from "@bananapus/core/src/structs/JBBeforePayRecordedContext.sol"; +import {JBMetadataResolver} from "@bananapus/core/src/libraries/JBMetadataResolver.sol"; import {JBAfterPayRecordedContext} from "@bananapus/core/src/structs/JBAfterPayRecordedContext.sol"; import {JBAfterRedeemRecordedContext} from "@bananapus/core/src/structs/JBAfterRedeemRecordedContext.sol"; +import {JBBeforePayRecordedContext} from "@bananapus/core/src/structs/JBBeforePayRecordedContext.sol"; import {JBBeforeRedeemRecordedContext} from "@bananapus/core/src/structs/JBBeforeRedeemRecordedContext.sol"; import {JBPayHookSpecification} from "@bananapus/core/src/structs/JBPayHookSpecification.sol"; import {JBRedeemHookSpecification} from "@bananapus/core/src/structs/JBRedeemHookSpecification.sol"; -import {JBMetadataResolver} from "@bananapus/core/src/libraries/JBMetadataResolver.sol"; +import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {mulDiv} from "@prb/math/src/Common.sol"; import {ERC721} from "./ERC721.sol"; import {IJB721Hook} from "../interfaces/IJB721Hook.sol"; @@ -31,10 +31,10 @@ abstract contract JB721Hook is ERC721, IJB721Hook, IJBRulesetDataHook, IJBPayHoo // --------------------------- custom errors ------------------------- // //*********************************************************************// - error INVALID_PAY(); - error INVALID_REDEEM(); - error UNAUTHORIZED_TOKEN(uint256 tokenId); - error UNEXPECTED_TOKEN_REDEEMED(); + error JB721Hook_InvalidPay(); + error JB721Hook_InvalidRedeem(); + error JB721Hook_UnauthorizedToken(); + error JB721Hook_UnexpectedTokenRedeemed(); //*********************************************************************// // --------------- public immutable stored properties ---------------- // @@ -54,14 +54,21 @@ abstract contract JB721Hook is ERC721, IJB721Hook, IJBRulesetDataHook, IJBPayHoo uint256 public override PROJECT_ID; //*********************************************************************// - // ------------------------- external views -------------------------- // + // -------------------------- constructor ---------------------------- // //*********************************************************************// - /// @notice Required by the IJBRulesetDataHook interfaces. Return false to not leak any permissions. - function hasMintPermissionFor(uint256, address) external pure returns (bool) { - return false; + /// @param directory A directory of terminals and controllers for projects. + constructor(IJBDirectory directory) { + DIRECTORY = directory; + // Store the address of the original hook deploy. Clones will each use the address of the instance they're based + // on. + METADATA_ID_TARGET = address(this); } + //*********************************************************************// + // ------------------------- external views -------------------------- // + //*********************************************************************// + /// @notice The data calculated before a payment is recorded in the terminal store. This data is provided to the /// terminal's `pay(...)` transaction. /// @dev Sets this contract as the pay hook. Part of `IJBRulesetDataHook`. @@ -69,7 +76,9 @@ abstract contract JB721Hook is ERC721, IJB721Hook, IJBRulesetDataHook, IJBPayHoo /// @return weight The new `weight` to use, overriding the ruleset's `weight`. /// @return hookSpecifications The amount and data to send to pay hooks (this contract) instead of adding to the /// terminal's balance. - function beforePayRecordedWith(JBBeforePayRecordedContext calldata context) + function beforePayRecordedWith( + JBBeforePayRecordedContext calldata context + ) public view virtual @@ -93,7 +102,9 @@ abstract contract JB721Hook is ERC721, IJB721Hook, IJBRulesetDataHook, IJBPayHoo /// @return totalSupply The total amount of tokens that are considered to be existing. /// @return hookSpecifications The amount and data to send to redeem hooks (this contract) instead of returning to /// the beneficiary. - function beforeRedeemRecordedWith(JBBeforeRedeemRecordedContext calldata context) + function beforeRedeemRecordedWith( + JBBeforeRedeemRecordedContext calldata context + ) public view virtual @@ -106,7 +117,7 @@ abstract contract JB721Hook is ERC721, IJB721Hook, IJBRulesetDataHook, IJBPayHoo ) { // Make sure (fungible) project tokens aren't also being redeemed. - if (context.redeemCount > 0) revert UNEXPECTED_TOKEN_REDEEMED(); + if (context.redeemCount > 0) revert JB721Hook_UnexpectedTokenRedeemed(); // Fetch the redeem hook metadata using the corresponding metadata ID. (bool metadataExists, bytes memory metadata) = @@ -131,6 +142,11 @@ abstract contract JB721Hook is ERC721, IJB721Hook, IJBRulesetDataHook, IJBPayHoo redemptionRate = context.redemptionRate; } + /// @notice Required by the IJBRulesetDataHook interfaces. Return false to not leak any permissions. + function hasMintPermissionFor(uint256, address) external pure returns (bool) { + return false; + } + //*********************************************************************// // -------------------------- public views --------------------------- // //*********************************************************************// @@ -154,10 +170,21 @@ abstract contract JB721Hook is ERC721, IJB721Hook, IJBRulesetDataHook, IJBPayHoo return 0; } + /// @notice Indicates if this contract adheres to the specified interface. + /// @dev See {IERC165-supportsInterface}. + /// @param _interfaceId The ID of the interface to check for adherence to. + function supportsInterface(bytes4 _interfaceId) public view virtual override(ERC721, IERC165) returns (bool) { + return _interfaceId == type(IJB721Hook).interfaceId || _interfaceId == type(IJBRulesetDataHook).interfaceId + || _interfaceId == type(IJBPayHook).interfaceId || _interfaceId == type(IJBRedeemHook).interfaceId + || _interfaceId == type(IERC2981).interfaceId || super.supportsInterface(_interfaceId); + } + /// @notice Calculates the cumulative redemption weight of all NFT token IDs. /// @param context The redemption context passed to this contract by the `redeemTokensOf(...)` function. /// @return The total cumulative redemption weight of all NFT token IDs. - function totalRedemptionWeight(JBBeforeRedeemRecordedContext calldata context) + function totalRedemptionWeight( + JBBeforeRedeemRecordedContext calldata context + ) public view virtual @@ -167,27 +194,10 @@ abstract contract JB721Hook is ERC721, IJB721Hook, IJBRulesetDataHook, IJBPayHoo return 0; } - /// @notice Indicates if this contract adheres to the specified interface. - /// @dev See {IERC165-supportsInterface}. - /// @param _interfaceId The ID of the interface to check for adherence to. - function supportsInterface(bytes4 _interfaceId) public view virtual override(ERC721, IERC165) returns (bool) { - return _interfaceId == type(IJB721Hook).interfaceId || _interfaceId == type(IJBRulesetDataHook).interfaceId - || _interfaceId == type(IJBPayHook).interfaceId || _interfaceId == type(IJBRedeemHook).interfaceId - || _interfaceId == type(IERC2981).interfaceId || super.supportsInterface(_interfaceId); - } - //*********************************************************************// - // -------------------------- constructor ---------------------------- // + // ------------------------ internal views --------------------------- // //*********************************************************************// - /// @param directory A directory of terminals and controllers for projects. - constructor(IJBDirectory directory) { - DIRECTORY = directory; - // Store the address of the original hook deploy. Clones will each use the address of the instance they're based - // on. - METADATA_ID_TARGET = address(this); - } - /// @notice Initializes the contract by associating it with a project and adding ERC721 details. /// @param projectId The ID of the project that this contract is associated with. /// @param name The name of the NFT collection. @@ -214,7 +224,7 @@ abstract contract JB721Hook is ERC721, IJB721Hook, IJBRulesetDataHook, IJBPayHoo if ( msg.value != 0 || !DIRECTORY.isTerminalOf(projectId, IJBTerminal(msg.sender)) || context.projectId != projectId - ) revert INVALID_PAY(); + ) revert JB721Hook_InvalidPay(); // Process the payment. _processPayment(context); @@ -234,7 +244,7 @@ abstract contract JB721Hook is ERC721, IJB721Hook, IJBRulesetDataHook, IJBPayHoo if ( msg.value != 0 || !DIRECTORY.isTerminalOf(projectId, IJBTerminal(msg.sender)) || context.projectId != projectId - ) revert INVALID_REDEEM(); + ) revert JB721Hook_InvalidRedeem(); // Fetch the redeem hook metadata using the corresponding metadata ID. (bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor( @@ -258,7 +268,7 @@ abstract contract JB721Hook is ERC721, IJB721Hook, IJBRulesetDataHook, IJBPayHoo tokenId = decodedTokenIds[i]; // Make sure the token's owner is correct. - if (_ownerOf(tokenId) != context.holder) revert UNAUTHORIZED_TOKEN(tokenId); + if (_ownerOf(tokenId) != context.holder) revert JB721Hook_UnauthorizedToken(); // Burn the token. _burn(tokenId); @@ -272,15 +282,11 @@ abstract contract JB721Hook is ERC721, IJB721Hook, IJBRulesetDataHook, IJBPayHoo // ---------------------- internal transactions ---------------------- // //*********************************************************************// - /// @notice Process a received payment. - /// @param context The payment context passed in by the terminal. - function _processPayment(JBAfterPayRecordedContext calldata context) internal virtual { - context; // Prevents unused var compiler and natspec complaints. - } - /// @notice Executes after NFTs have been burned via redemption. /// @param tokenIds The token IDs of the NFTs that were burned. - function _didBurn(uint256[] memory tokenIds) internal virtual { - tokenIds; - } + function _didBurn(uint256[] memory tokenIds) internal virtual; + + /// @notice Process a received payment. + /// @param context The payment context passed in by the terminal. + function _processPayment(JBAfterPayRecordedContext calldata context) internal virtual; } diff --git a/src/interfaces/IJB721Hook.sol b/src/interfaces/IJB721Hook.sol index e8cf1466..a4c673fc 100644 --- a/src/interfaces/IJB721Hook.sol +++ b/src/interfaces/IJB721Hook.sol @@ -4,9 +4,7 @@ pragma solidity ^0.8.0; import {IJBDirectory} from "@bananapus/core/src/interfaces/IJBDirectory.sol"; interface IJB721Hook { - function PROJECT_ID() external view returns (uint256); - function DIRECTORY() external view returns (IJBDirectory); - function METADATA_ID_TARGET() external view returns (address); + function PROJECT_ID() external view returns (uint256); } diff --git a/src/interfaces/IJB721TiersHook.sol b/src/interfaces/IJB721TiersHook.sol index e765a4c6..bd019295 100644 --- a/src/interfaces/IJB721TiersHook.sol +++ b/src/interfaces/IJB721TiersHook.sol @@ -1,18 +1,23 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {IJBRulesets} from "@bananapus/core/src/interfaces/IJBRulesets.sol"; import {IJBPrices} from "@bananapus/core/src/interfaces/IJBPrices.sol"; +import {IJBRulesets} from "@bananapus/core/src/interfaces/IJBRulesets.sol"; import {IJB721Hook} from "./IJB721Hook.sol"; -import {IJB721TokenUriResolver} from "./IJB721TokenUriResolver.sol"; import {IJB721TiersHookStore} from "./IJB721TiersHookStore.sol"; -import {JB721InitTiersConfig} from "./../structs/JB721InitTiersConfig.sol"; -import {JB721TierConfig} from "./../structs/JB721TierConfig.sol"; -import {JB721TiersHookFlags} from "./../structs/JB721TiersHookFlags.sol"; -import {JB721TiersMintReservesConfig} from "./../structs/JB721TiersMintReservesConfig.sol"; +import {IJB721TokenUriResolver} from "./IJB721TokenUriResolver.sol"; +import {JB721InitTiersConfig} from "../structs/JB721InitTiersConfig.sol"; +import {JB721TierConfig} from "../structs/JB721TierConfig.sol"; +import {JB721TiersHookFlags} from "../structs/JB721TiersHookFlags.sol"; +import {JB721TiersMintReservesConfig} from "../structs/JB721TiersMintReservesConfig.sol"; +import {JB721TiersSetDiscountPercentConfig} from "../structs/JB721TiersSetDiscountPercentConfig.sol"; interface IJB721TiersHook is IJB721Hook { + event AddPayCredits( + uint256 indexed amount, uint256 indexed newTotalCredits, address indexed account, address caller + ); + event AddTier(uint256 indexed tierId, JB721TierConfig tier, address caller); event Mint( uint256 indexed tokenId, uint256 indexed tierId, @@ -20,71 +25,49 @@ interface IJB721TiersHook is IJB721Hook { uint256 totalAmountPaid, address caller ); - event MintReservedNft(uint256 indexed tokenId, uint256 indexed tierId, address indexed beneficiary, address caller); - - event AddTier(uint256 indexed tierId, JB721TierConfig tier, address caller); - event RemoveTier(uint256 indexed tierId, address caller); - - event SetEncodedIPFSUri(uint256 indexed tierId, bytes32 encodedIPFSUri, address caller); - event SetBaseUri(string indexed baseUri, address caller); - - event SetContractUri(string indexed contractUri, address caller); - - event SetTokenUriResolver(IJB721TokenUriResolver indexed newResolver, address caller); - - event AddPayCredits( - uint256 indexed amount, uint256 indexed newTotalCredits, address indexed account, address caller - ); - + event SetContractUri(string indexed uri, address caller); + event SetDiscountPercent(uint256 indexed tierId, uint256 discountPercent, address caller); + event SetEncodedIPFSUri(uint256 indexed tierId, bytes32 encodedUri, address caller); + event SetTokenUriResolver(IJB721TokenUriResolver indexed resolver, address caller); event UsePayCredits( uint256 indexed amount, uint256 indexed newTotalCredits, address indexed account, address caller ); - function STORE() external view returns (IJB721TiersHookStore); - function RULESETS() external view returns (IJBRulesets); - - function pricingContext() external view returns (uint256, uint256, IJBPrices); - - function payCreditsOf(address addr) external view returns (uint256); - - function firstOwnerOf(uint256 tokenId) external view returns (address); + function STORE() external view returns (IJB721TiersHookStore); function baseURI() external view returns (string memory); - function contractURI() external view returns (string memory); + function firstOwnerOf(uint256 tokenId) external view returns (address); + function payCreditsOf(address addr) external view returns (uint256); + function pricingContext() external view returns (uint256, uint256, IJBPrices); - function adjustTiers(JB721TierConfig[] memory tierDataToAdd, uint256[] memory tierIdsToRemove) external; - - function mintPendingReservesFor(JB721TiersMintReservesConfig[] memory reserveMintConfigs) external; - - function mintPendingReservesFor(uint256 tierId, uint256 count) external; - - function mintFor(uint16[] calldata tierIds, address beneficiary) external returns (uint256[] memory tokenIds); - - function setMetadata( - string memory baseUri, - string calldata contractMetadataUri, - IJB721TokenUriResolver tokenUriResolver, - uint256 encodedIPFSUriTierId, - bytes32 encodedIPFSUri - ) - external; - + function adjustTiers(JB721TierConfig[] calldata tierDataToAdd, uint256[] calldata tierIdsToRemove) external; function initialize( uint256 projectId, string memory name, string memory symbol, - IJBRulesets rulesets, string memory baseUri, IJB721TokenUriResolver tokenUriResolver, string memory contractUri, JB721InitTiersConfig memory tiersConfig, - IJB721TiersHookStore store, JB721TiersHookFlags memory flags ) external; + function setDiscountPercentOf(uint256 tierId, uint256 discountPercent) external; + function setDiscountPercentsOf(JB721TiersSetDiscountPercentConfig[] calldata configs) external; + function mintFor(uint16[] calldata tierIds, address beneficiary) external returns (uint256[] memory tokenIds); + function mintPendingReservesFor(JB721TiersMintReservesConfig[] calldata reserveMintConfigs) external; + function mintPendingReservesFor(uint256 tierId, uint256 count) external; + function setMetadata( + string calldata baseUri, + string calldata contractMetadataUri, + IJB721TokenUriResolver tokenUriResolver, + uint256 encodedIPFSUriTierId, + bytes32 encodedIPFSUri + ) + external; } diff --git a/src/interfaces/IJB721TiersHookDeployer.sol b/src/interfaces/IJB721TiersHookDeployer.sol index b8a14207..930007a6 100644 --- a/src/interfaces/IJB721TiersHookDeployer.sol +++ b/src/interfaces/IJB721TiersHookDeployer.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {JBDeploy721TiersHookConfig} from "../structs/JBDeploy721TiersHookConfig.sol"; import {IJB721TiersHook} from "./IJB721TiersHook.sol"; +import {JBDeploy721TiersHookConfig} from "../structs/JBDeploy721TiersHookConfig.sol"; interface IJB721TiersHookDeployer { - event HookDeployed(uint256 indexed projectId, IJB721TiersHook newHook); + event HookDeployed(uint256 indexed projectId, IJB721TiersHook hook, address caller); function deployHookFor( uint256 projectId, diff --git a/src/interfaces/IJB721TiersHookProjectDeployer.sol b/src/interfaces/IJB721TiersHookProjectDeployer.sol index e079f568..14d40013 100644 --- a/src/interfaces/IJB721TiersHookProjectDeployer.sol +++ b/src/interfaces/IJB721TiersHookProjectDeployer.sol @@ -4,15 +4,14 @@ pragma solidity ^0.8.0; import {IJBDirectory} from "@bananapus/core/src/interfaces/IJBDirectory.sol"; import {IJBController} from "@bananapus/core/src/interfaces/IJBController.sol"; +import {IJB721TiersHookDeployer} from "./IJB721TiersHookDeployer.sol"; import {JBDeploy721TiersHookConfig} from "../structs/JBDeploy721TiersHookConfig.sol"; import {JBLaunchProjectConfig} from "../structs/JBLaunchProjectConfig.sol"; import {JBLaunchRulesetsConfig} from "../structs/JBLaunchRulesetsConfig.sol"; import {JBQueueRulesetsConfig} from "../structs/JBQueueRulesetsConfig.sol"; -import {IJB721TiersHookDeployer} from "./IJB721TiersHookDeployer.sol"; interface IJB721TiersHookProjectDeployer { function DIRECTORY() external view returns (IJBDirectory); - function HOOK_DEPLOYER() external view returns (IJB721TiersHookDeployer); function launchProjectFor( diff --git a/src/interfaces/IJB721TiersHookStore.sol b/src/interfaces/IJB721TiersHookStore.sol index 72b24942..f4855fe0 100644 --- a/src/interfaces/IJB721TiersHookStore.sol +++ b/src/interfaces/IJB721TiersHookStore.sol @@ -2,18 +2,36 @@ pragma solidity ^0.8.0; import {IJB721TokenUriResolver} from "./IJB721TokenUriResolver.sol"; -import {JB721TierConfig} from "../structs/JB721TierConfig.sol"; import {JB721Tier} from "../structs/JB721Tier.sol"; +import {JB721TierConfig} from "../structs/JB721TierConfig.sol"; import {JB721TiersHookFlags} from "../structs/JB721TiersHookFlags.sol"; interface IJB721TiersHookStore { event CleanTiers(address indexed hook, address caller); - function totalSupplyOf(address hook) external view returns (uint256); - function balanceOf(address hook, address owner) external view returns (uint256); - + function defaultReserveBeneficiaryOf(address hook) external view returns (address); + function encodedIPFSUriOf(address hook, uint256 tierId) external view returns (bytes32); + function encodedTierIPFSUriOf(address hook, uint256 tokenId) external view returns (bytes32); + function flagsOf(address hook) external view returns (JB721TiersHookFlags memory); + function isTierRemoved(address hook, uint256 tierId) external view returns (bool); function maxTierIdOf(address hook) external view returns (uint256); + function numberOfBurnedFor(address hook, uint256 tierId) external view returns (uint256); + function numberOfPendingReservesFor(address hook, uint256 tierId) external view returns (uint256); + function numberOfReservesMintedFor(address hook, uint256 tierId) external view returns (uint256); + function redemptionWeightOf(address hook, uint256[] calldata tokenIds) external view returns (uint256 weight); + function reserveBeneficiaryOf(address hook, uint256 tierId) external view returns (address); + function tierBalanceOf(address hook, address owner, uint256 tier) external view returns (uint256); + function tierIdOfToken(uint256 tokenId) external pure returns (uint256); + function tierOf(address hook, uint256 id, bool includeResolvedUri) external view returns (JB721Tier memory tier); + function tierOfTokenId( + address hook, + uint256 tokenId, + bool includeResolvedUri + ) + external + view + returns (JB721Tier memory tier); function tiersOf( address hook, @@ -26,55 +44,16 @@ interface IJB721TiersHookStore { view returns (JB721Tier[] memory tiers); - function tierOf(address hook, uint256 id, bool includeResolvedUri) external view returns (JB721Tier memory tier); - - function tierBalanceOf(address hook, address owner, uint256 tier) external view returns (uint256); - - function tierOfTokenId( - address hook, - uint256 tokenId, - bool includeResolvedUri - ) - external - view - returns (JB721Tier memory tier); - - function tierIdOfToken(uint256 tokenId) external pure returns (uint256); - - function encodedIPFSUriOf(address hook, uint256 tierId) external view returns (bytes32); - - function redemptionWeightOf(address hook, uint256[] memory tokenIds) external view returns (uint256 weight); - - function totalRedemptionWeight(address hook) external view returns (uint256 weight); - - function numberOfPendingReservesFor(address hook, uint256 tierId) external view returns (uint256); - - function numberOfReservesMintedFor(address hook, uint256 tierId) external view returns (uint256); - - function numberOfBurnedFor(address hook, uint256 tierId) external view returns (uint256); - - function isTierRemoved(address hook, uint256 tierId) external view returns (bool); - - function flagsOf(address hook) external view returns (JB721TiersHookFlags memory); - - function votingUnitsOf(address hook, address account) external view returns (uint256 units); - function tierVotingUnitsOf(address hook, address account, uint256 tierId) external view returns (uint256 units); - - function defaultReserveBeneficiaryOf(address hook) external view returns (address); - - function reserveBeneficiaryOf(address hook, uint256 tierId) external view returns (address); - function tokenUriResolverOf(address hook) external view returns (IJB721TokenUriResolver); + function totalRedemptionWeight(address hook) external view returns (uint256 weight); + function totalSupplyOf(address hook) external view returns (uint256); + function votingUnitsOf(address hook, address account) external view returns (uint256 units); - function encodedTierIPFSUriOf(address hook, uint256 tokenId) external view returns (bytes32); - - function recordAddTiers(JB721TierConfig[] memory tierData) external returns (uint256[] memory tierIds); - - function recordMintReservesFor(uint256 tierId, uint256 count) external returns (uint256[] memory tokenIds); - - function recordBurn(uint256[] memory tokenIds) external; - + function cleanTiers(address hook) external; + function recordAddTiers(JB721TierConfig[] calldata tierData) external returns (uint256[] memory tierIds); + function recordBurn(uint256[] calldata tokenIds) external; + function recordFlags(JB721TiersHookFlags calldata flag) external; function recordMint( uint256 amount, uint16[] calldata tierIds, @@ -82,16 +61,10 @@ interface IJB721TiersHookStore { ) external returns (uint256[] memory tokenIds, uint256 leftoverAmount); - - function recordTransferForTier(uint256 tierId, address from, address to) external; - - function recordRemoveTierIds(uint256[] memory tierIds) external; - - function recordSetTokenUriResolver(IJB721TokenUriResolver resolver) external; - + function recordMintReservesFor(uint256 tierId, uint256 count) external returns (uint256[] memory tokenIds); + function recordRemoveTierIds(uint256[] calldata tierIds) external; function recordSetEncodedIPFSUriOf(uint256 tierId, bytes32 encodedIPFSUri) external; - - function recordFlags(JB721TiersHookFlags calldata flag) external; - - function cleanTiers(address hook) external; + function recordSetDiscountPercentOf(uint256 tierId, uint256 discountPercent) external; + function recordSetTokenUriResolver(IJB721TokenUriResolver resolver) external; + function recordTransferForTier(uint256 tierId, address from, address to) external; } diff --git a/src/libraries/JB721Constants.sol b/src/libraries/JB721Constants.sol new file mode 100644 index 00000000..3c0b9053 --- /dev/null +++ b/src/libraries/JB721Constants.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @notice Global constants used across 721 hook contracts. +library JB721Constants { + uint16 public constant MAX_DISCOUNT_PERCENT = 200; +} diff --git a/src/libraries/JB721TiersRulesetMetadataResolver.sol b/src/libraries/JB721TiersRulesetMetadataResolver.sol index cd62b2d6..5775eae0 100644 --- a/src/libraries/JB721TiersRulesetMetadataResolver.sol +++ b/src/libraries/JB721TiersRulesetMetadataResolver.sol @@ -18,7 +18,9 @@ library JB721TiersRulesetMetadataResolver { /// @notice Pack the ruleset metadata for the 721 hook into a single `uint256`. /// @param metadata The metadata to validate and pack. /// @return packed A `uint256` containing the packed metadata for the 721 hook. - function pack721TiersRulesetMetadata(JB721TiersRulesetMetadata memory metadata) + function pack721TiersRulesetMetadata( + JB721TiersRulesetMetadata memory metadata + ) internal pure returns (uint256 packed) diff --git a/src/structs/JB721InitTiersConfig.sol b/src/structs/JB721InitTiersConfig.sol index 6702e458..ef1a184b 100644 --- a/src/structs/JB721InitTiersConfig.sol +++ b/src/structs/JB721InitTiersConfig.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import {IJBPrices} from "@bananapus/core/src/interfaces/IJBPrices.sol"; + import {JB721TierConfig} from "./JB721TierConfig.sol"; /// @notice Config to initialize a `JB721TiersHook` with tiers and price data. diff --git a/src/structs/JB721Tier.sol b/src/structs/JB721Tier.sol index 135ffab5..3774c4cf 100644 --- a/src/structs/JB721Tier.sol +++ b/src/structs/JB721Tier.sol @@ -12,9 +12,11 @@ pragma solidity ^0.8.0; /// @custom:member reserveBeneficiary The address which receives any reserve NFTs from this tier. /// @custom:member encodedIPFSUri The IPFS URI to use for each NFT in this tier. /// @custom:member category The category that NFTs in this tier belongs to. Used to group NFT tiers. +/// @custom:member discountPercent The discount that should be applied to the tier. /// @custom:member allowOwnerMint A boolean indicating whether the contract's owner can mint NFTs from this tier /// on-demand. /// @custom:member cannotBeRemoved A boolean indicating whether attempts to remove this tier will revert. +/// @custom:member cannotIncreaseDiscountPercent If the tier cannot have its discount increased. /// @custom:member transfersPausable A boolean indicating whether transfers for NFTs in tier can be paused. /// @custom:member resolvedUri A resolved token URI for NFTs in this tier. Only available if the NFT this tier belongs /// to has a resolver. @@ -28,8 +30,10 @@ struct JB721Tier { address reserveBeneficiary; bytes32 encodedIPFSUri; uint24 category; + uint8 discountPercent; bool allowOwnerMint; bool transfersPausable; bool cannotBeRemoved; + bool cannotIncreaseDiscountPercent; string resolvedUri; } diff --git a/src/structs/JB721TierConfig.sol b/src/structs/JB721TierConfig.sol index f439860d..2befdeef 100644 --- a/src/structs/JB721TierConfig.sol +++ b/src/structs/JB721TierConfig.sol @@ -12,6 +12,7 @@ pragma solidity ^0.8.0; /// reserve beneficiary if one is set. /// @custom:member encodedIPFSUri The IPFS URI to use for each NFT in this tier. /// @custom:member category The category that NFTs in this tier belongs to. Used to group NFT tiers. +/// @custom:member discountPercent The discount that should be applied to the tier. /// @custom:member allowOwnerMint A boolean indicating whether the contract's owner can mint NFTs from this tier /// on-demand. /// @custom:member useReserveBeneficiaryAsDefault A boolean indicating whether this tier's `reserveBeneficiary` should @@ -20,6 +21,7 @@ pragma solidity ^0.8.0; /// @custom:member useVotingUnits A boolean indicating whether the `votingUnits` should be used to calculate voting /// power. If `useVotingUnits` is false, voting power is based on the tier's price. /// @custom:member cannotBeRemoved If the tier cannot be removed once added. +/// @custom:member cannotIncreaseDiscount If the tier cannot have its discount increased. struct JB721TierConfig { uint104 price; uint32 initialSupply; @@ -28,9 +30,11 @@ struct JB721TierConfig { address reserveBeneficiary; bytes32 encodedIPFSUri; uint24 category; + uint8 discountPercent; bool allowOwnerMint; bool useReserveBeneficiaryAsDefault; bool transfersPausable; bool useVotingUnits; bool cannotBeRemoved; + bool cannotIncreaseDiscountPercent; } diff --git a/src/structs/JB721TiersSetDiscountPercentConfig.sol b/src/structs/JB721TiersSetDiscountPercentConfig.sol new file mode 100644 index 00000000..cb3bd797 --- /dev/null +++ b/src/structs/JB721TiersSetDiscountPercentConfig.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @custom:member tierId The ID of the tier to set the discount percent for. +/// @custom:member discountPercent The discount percent to set for the tier. +struct JB721TiersSetDiscountPercentConfig { + uint32 tierId; + uint16 discountPercent; +} diff --git a/src/structs/JBDeploy721TiersHookConfig.sol b/src/structs/JBDeploy721TiersHookConfig.sol index 74323353..f2fae8f3 100644 --- a/src/structs/JBDeploy721TiersHookConfig.sol +++ b/src/structs/JBDeploy721TiersHookConfig.sol @@ -1,25 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {IJBRulesets} from "@bananapus/core/src/interfaces/IJBRulesets.sol"; -import {IJB721TokenUriResolver} from "../interfaces/IJB721TokenUriResolver.sol"; import {JB721InitTiersConfig} from "./JB721InitTiersConfig.sol"; import {JB721TiersHookFlags} from "./JB721TiersHookFlags.sol"; +import {IJB721TokenUriResolver} from "../interfaces/IJB721TokenUriResolver.sol"; /// @custom:member name The NFT collection's name. /// @custom:member symbol The NFT collection's symbol. -/// @custom:member rulesets The contract storing and managing project rulesets. /// @custom:member baseUri The URI to use as a base for full NFT URIs. /// @custom:member tokenUriResolver The contract responsible for resolving the URI for each NFT. /// @custom:member contractUri The URI where this contract's metadata can be found. /// @custom:member tiersConfig The NFT tiers and pricing config to launch the hook with. /// @custom:member reserveBeneficiary The default reserved beneficiary for all tiers. -/// @custom:member store The contract to store and manage this hook's data. /// @custom:member flags A set of boolean options to configure the hook with. struct JBDeploy721TiersHookConfig { string name; string symbol; - IJBRulesets rulesets; string baseUri; IJB721TokenUriResolver tokenUriResolver; string contractUri; diff --git a/src/structs/JBPayDataHookRulesetConfig.sol b/src/structs/JBPayDataHookRulesetConfig.sol index 0b478106..67a1b1ba 100644 --- a/src/structs/JBPayDataHookRulesetConfig.sol +++ b/src/structs/JBPayDataHookRulesetConfig.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.0; import {IJBRulesetApprovalHook} from "@bananapus/core/src/interfaces/IJBRulesetApprovalHook.sol"; -import {JBSplitGroup} from "@bananapus/core/src/structs/JBSplitGroup.sol"; import {JBFundAccessLimitGroup} from "@bananapus/core/src/structs/JBFundAccessLimitGroup.sol"; +import {JBSplitGroup} from "@bananapus/core/src/structs/JBSplitGroup.sol"; import {JBPayDataHookRulesetMetadata} from "./JBPayDataHookRulesetMetadata.sol"; diff --git a/src/structs/JBPayDataHookRulesetMetadata.sol b/src/structs/JBPayDataHookRulesetMetadata.sol index 188a5757..17b3be9c 100644 --- a/src/structs/JBPayDataHookRulesetMetadata.sol +++ b/src/structs/JBPayDataHookRulesetMetadata.sol @@ -19,6 +19,8 @@ pragma solidity ^0.8.0; /// terminals to use. /// @custom:member allowAddPriceFeed A flag indicating if a project can add new price feeds to calculate exchange rates /// between its tokens. +/// @custom:member allowCrosschainSuckerExtension A flag indicating if the crosschain sucker extension should be +/// allowed during this ruleset. /// @custom:member holdFees A flag indicating if fees should be held during this ruleset. /// @custom:member useTotalSurplusForRedemptions A flag indicating if redemptions should use the project's balance held /// in all terminals instead of the project's local terminal balance from which the redemption is being fulfilled. @@ -37,6 +39,7 @@ struct JBPayDataHookRulesetMetadata { bool allowSetController; bool allowAddAccountingContext; bool allowAddPriceFeed; + bool allowCrosschainSuckerExtension; bool ownerMustSendPayouts; bool holdFees; bool useTotalSurplusForRedemptions; diff --git a/src/structs/JBStored721Tier.sol b/src/structs/JBStored721Tier.sol index 8b222e5f..b2e17421 100644 --- a/src/structs/JBStored721Tier.sol +++ b/src/structs/JBStored721Tier.sol @@ -6,6 +6,7 @@ pragma solidity ^0.8.0; /// @custom:member initialSupply The total number of NFTs which can be minted from this tier. /// @custom:member votingUnits The number of votes that each NFT in this tier gets. /// @custom:member category The category that NFTs in this tier belongs to. Used to group NFT tiers. +/// @custom:member discountPercent The discount that should be applied to the tier. /// @custom:member reserveFrequency The frequency at which an extra NFT is minted for the `reserveBeneficiary` from this /// tier. With a `reserveFrequency` of 5, an extra NFT will be minted for the `reserveBeneficiary` for every 5 NFTs /// purchased. @@ -18,8 +19,9 @@ struct JBStored721Tier { uint104 price; uint32 remainingSupply; uint32 initialSupply; - uint40 votingUnits; + uint32 votingUnits; uint24 category; + uint8 discountPercent; uint16 reserveFrequency; uint8 packedBools; } diff --git a/test/E2E/Pay_Mint_Redeem_E2E.t.sol b/test/E2E/Pay_Mint_Redeem_E2E.t.sol index b43587a0..8801ad0a 100644 --- a/test/E2E/Pay_Mint_Redeem_E2E.t.sol +++ b/test/E2E/Pay_Mint_Redeem_E2E.t.sol @@ -16,6 +16,8 @@ import {MetadataResolverHelper} from "@bananapus/core/test/helpers/MetadataResol contract Test_TiersHook_E2E is TestBaseWorkflow { using JBRulesetMetadataResolver for JBRuleset; + uint256 totalSupplyAfterPay; + address reserveBeneficiary = address(bytes20(keccak256("reserveBeneficiary"))); address trustedForwarder = address(123_456); @@ -56,9 +58,9 @@ contract Test_TiersHook_E2E is TestBaseWorkflow { function setUp() public override { super.setUp(); - hook = new JB721TiersHook(jbDirectory, jbPermissions, trustedForwarder); - addressRegistry = new JBAddressRegistry(); store = new JB721TiersHookStore(); + hook = new JB721TiersHook(jbDirectory, jbPermissions, jbRulesets, store, trustedForwarder); + addressRegistry = new JBAddressRegistry(); JB721TiersHookDeployer hookDeployer = new JB721TiersHookDeployer(hook, store, addressRegistry, trustedForwarder); deployer = new JB721TiersHookProjectDeployer(IJBDirectory(jbDirectory), IJBPermissions(jbPermissions), hookDeployer); @@ -148,6 +150,105 @@ contract Test_TiersHook_E2E is TestBaseWorkflow { assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary); } + function testFuzzMintWithDiscountOnPayIfOneTierIsPassed(uint256 tierStartPrice, uint256 discountPercent) external { + // Cap our fuzzed params + tierStartPrice = bound(tierStartPrice, 1, type(uint208).max - 1); + discountPercent = bound(discountPercent, 1, 200); + + { + uint256 amountMinted = (tierStartPrice * 1000) / 2; + totalSupplyAfterPay += amountMinted; + } + + // Cap the highest tier ID. + uint256 highestTier = 1; + + (JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) = + createDiscountedData(tierStartPrice, uint8(discountPercent)); + uint256 projectId = deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController); + + // Crafting the payment metadata: add the highest tier ID. + uint16[] memory rawMetadata = new uint16[](1); + rawMetadata[0] = uint16(highestTier); + + // Build the metadata using the tiers to mint and the overspending flag. + bytes[] memory data = new bytes[](1); + data[0] = abi.encode(true, rawMetadata); + + address dataHook = jbRulesets.currentOf(projectId).dataHook(); + + // Pass the hook ID. + bytes4[] memory ids = new bytes4[](1); + ids[0] = JBMetadataResolver.getId("pay", address(hook)); + + // Generate the metadata. + bytes memory hookMetadata = metadataHelper.createMetadata(ids, data); + + /* // Check: was an NFT with the correct tier ID and token ID minted? + vm.expectEmit(true, true, true, true); + emit Mint( + _generateTokenId(highestTier, 1), + highestTier, + beneficiary, + tierStartPrice, + address(jbMultiTerminal) // msg.sender + ); */ + + if (totalSupplyAfterPay > type(uint208).max) { + vm.expectRevert(JBTokens.JBTokens_OverflowAlert.selector); + } + + // Pay the terminal to mint the NFTs. + vm.deal(caller, type(uint256).max); + vm.prank(caller); + jbMultiTerminal.pay{value: tierStartPrice}({ + projectId: projectId, + amount: tierStartPrice, + token: JBConstants.NATIVE_TOKEN, + beneficiary: beneficiary, + minReturnedTokens: 0, + memo: "Take my money!", + metadata: hookMetadata + }); + + if (totalSupplyAfterPay < type(uint208).max) { + if (tierStartPrice > type(uint104).max) { + uint256 expectedDiscount = + mulDiv(uint104(tierStartPrice), discountPercent, JB721Constants.MAX_DISCOUNT_PERCENT); + uint256 paidForNft = uint104(tierStartPrice) - expectedDiscount; + + // Check: should be credited tierStartPrice minus what you paid for the NFT plus the discount + assertEq(IJB721TiersHook(dataHook).payCreditsOf(beneficiary), tierStartPrice - paidForNft); + } else { + uint256 expectedCredits = mulDiv(tierStartPrice, discountPercent, JB721Constants.MAX_DISCOUNT_PERCENT); + assertEq(IJB721TiersHook(dataHook).payCreditsOf(beneficiary), expectedCredits); + } + + { + // Check: did the beneficiary receive the NFT? + assertEq(IERC721(dataHook).balanceOf(beneficiary), 1); + + uint256 tokenId = _generateTokenId(highestTier, 1); + + // Check: is the beneficiary the first owner of the NFT? + assertEq(IERC721(dataHook).ownerOf(tokenId), beneficiary); + assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary); + + // Check: after a transfer, are the `firstOwnerOf` and `ownerOf` still correct? + vm.prank(beneficiary); + IERC721(dataHook).transferFrom(beneficiary, address(696_969_420), tokenId); + assertEq(IERC721(dataHook).ownerOf(tokenId), address(696_969_420)); + assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary); + + // Check: is the same true after a second transfer? + vm.prank(address(696_969_420)); + IERC721(dataHook).transferFrom(address(696_969_420), address(123_456_789), tokenId); + assertEq(IERC721(dataHook).ownerOf(tokenId), address(123_456_789)); + assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary); + } + } + } + function testMintOnPayIfMultipleTiersArePassed() external { (JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) = createData(); @@ -284,7 +385,9 @@ contract Test_TiersHook_E2E is TestBaseWorkflow { assertEq(IJB721TiersHook(dataHook).STORE().numberOfPendingReservesFor(dataHook, highestTier), 0); // Check: cannot mint pending reserves (since none should be pending)? - vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.INSUFFICIENT_PENDING_RESERVES.selector)); + vm.expectRevert( + abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientPendingReserves.selector) + ); vm.prank(projectOwner); IJB721TiersHook(dataHook).mintPendingReservesFor(highestTier, 1); @@ -345,7 +448,9 @@ contract Test_TiersHook_E2E is TestBaseWorkflow { // Check: there should now be 0 pending reserves. assertEq(IJB721TiersHook(dataHook).STORE().numberOfPendingReservesFor(dataHook, highestTier), 0); // Check: it should not be possible to mint pending reserves now (since there are none left). - vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.INSUFFICIENT_PENDING_RESERVES.selector)); + vm.expectRevert( + abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientPendingReserves.selector) + ); vm.prank(projectOwner); IJB721TiersHook(dataHook).mintPendingReservesFor(highestTier, 1); } @@ -586,17 +691,18 @@ contract Test_TiersHook_E2E is TestBaseWorkflow { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[i], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: false, - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); } tiersHookConfig = JBDeploy721TiersHookConfig({ name: name, symbol: symbol, - rulesets: jbRulesets, baseUri: baseUri, tokenUriResolver: IJB721TokenUriResolver(address(0)), contractUri: contractUri, @@ -628,6 +734,101 @@ contract Test_TiersHook_E2E is TestBaseWorkflow { ownerMustSendPayouts: false, allowAddAccountingContext: false, allowAddPriceFeed: false, + allowCrosschainSuckerExtension: false, + holdFees: false, + useTotalSurplusForRedemptions: false, + useDataHookForRedeem: true, + metadata: 0x00 + }); + + JBPayDataHookRulesetConfig[] memory rulesetConfigurations = new JBPayDataHookRulesetConfig[](1); + // Package up the ruleset configuration. + rulesetConfigurations[0].mustStartAtOrAfter = 0; + rulesetConfigurations[0].duration = 14; + rulesetConfigurations[0].weight = 1000 * 10 ** 18; + rulesetConfigurations[0].decayPercent = 450_000_000; + rulesetConfigurations[0].approvalHook = IJBRulesetApprovalHook(address(0)); + rulesetConfigurations[0].metadata = metadata; + + JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1); + JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1); + accountingContextsToAccept[0] = JBAccountingContext({ + token: JBConstants.NATIVE_TOKEN, + currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), + decimals: 18 + }); + terminalConfigurations[0] = + JBTerminalConfig({terminal: jbMultiTerminal, accountingContextsToAccept: accountingContextsToAccept}); + + launchProjectConfig = JBLaunchProjectConfig({ + projectUri: projectUri, + rulesetConfigurations: rulesetConfigurations, + terminalConfigurations: terminalConfigurations, + memo: "" + }); + } + + function createDiscountedData( + uint256 _price, + uint8 _discountPercent + ) + internal + view + returns (JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) + { + JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1); + tierConfigs[0] = JB721TierConfig({ + price: uint104(_price), + initialSupply: uint32(10), + votingUnits: uint32(10), + reserveFrequency: 10, + reserveBeneficiary: reserveBeneficiary, + encodedIPFSUri: tokenUris[0], + category: uint24(100), + discountPercent: _discountPercent, + allowOwnerMint: false, + useReserveBeneficiaryAsDefault: false, + transfersPausable: false, + useVotingUnits: false, + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false + }); + + tiersHookConfig = JBDeploy721TiersHookConfig({ + name: name, + symbol: symbol, + baseUri: baseUri, + tokenUriResolver: IJB721TokenUriResolver(address(0)), + contractUri: contractUri, + tiersConfig: JB721InitTiersConfig({ + tiers: tierConfigs, + currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), + decimals: 18, + prices: IJBPrices(address(0)) + }), + reserveBeneficiary: reserveBeneficiary, + flags: JB721TiersHookFlags({ + preventOverspending: false, + noNewTiersWithReserves: false, + noNewTiersWithVotes: false, + noNewTiersWithOwnerMinting: true + }) + }); + + JBPayDataHookRulesetMetadata memory metadata = JBPayDataHookRulesetMetadata({ + reservedPercent: 5000, //50% + redemptionRate: 5000, //50% + baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)), + pausePay: false, + pauseCreditTransfers: false, + allowOwnerMinting: true, + allowTerminalMigration: false, + allowSetTerminals: false, + allowSetController: false, + ownerMustSendPayouts: false, + allowAddAccountingContext: false, + allowAddPriceFeed: false, + allowCrosschainSuckerExtension: false, holdFees: false, useTotalSurplusForRedemptions: false, useDataHookForRedeem: true, diff --git a/test/unit/adjustTier_Unit.t.sol b/test/unit/adjustTier_Unit.t.sol index 771bb3ba..60baf84c 100644 --- a/test/unit/adjustTier_Unit.t.sol +++ b/test/unit/adjustTier_Unit.t.sol @@ -542,11 +542,13 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: true, - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); tiers[i] = JB721Tier({ id: uint32(i + 1), @@ -558,9 +560,11 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: i == 0 ? address(0) : tierConfigs[i].reserveBeneficiary, encodedIPFSUri: tierConfigs[i].encodedIPFSUri, category: tierConfigs[i].category, + discountPercent: tierConfigs[i].discountPercent, allowOwnerMint: tierConfigs[i].allowOwnerMint, transfersPausable: tierConfigs[i].transfersPausable, cannotBeRemoved: tierConfigs[i].cannotBeRemoved, + cannotIncreaseDiscountPercent: tierConfigs[i].cannotIncreaseDiscountPercent, resolvedUri: "" }); } @@ -661,11 +665,13 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: true, - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); tiers[i] = JB721Tier({ id: uint32(i + 1), @@ -677,21 +683,21 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: i == 0 ? address(0) : tierConfigs[i].reserveBeneficiary, encodedIPFSUri: tierConfigs[i].encodedIPFSUri, category: tierConfigs[i].category, + discountPercent: tierConfigs[i].discountPercent, allowOwnerMint: tierConfigs[i].allowOwnerMint, transfersPausable: tierConfigs[i].transfersPausable, cannotBeRemoved: tierConfigs[i].cannotBeRemoved, + cannotIncreaseDiscountPercent: tierConfigs[i].cannotIncreaseDiscountPercent, resolvedUri: "" }); } // Deploy the hook and its store with the initial tiers. - JB721TiersHookStore store = new JB721TiersHookStore(); vm.etch(hook_i, address(hook).code); JB721TiersHook hook = JB721TiersHook(hook_i); hook.initialize( projectId, name, symbol, - IJBRulesets(mockJBRulesets), baseUri, IJB721TokenUriResolver(mockTokenUriResolver), contractUri, @@ -701,7 +707,6 @@ contract Test_adjustTier_Unit is UnitTestSetup { decimals: 18, prices: IJBPrices(address(0)) }), - IJB721TiersHookStore(address(store)), JB721TiersHookFlags({ preventOverspending: false, noNewTiersWithReserves: false, @@ -726,11 +731,13 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: true, - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); tiersRemaining[arrayIndex] = JB721Tier({ id: uint32(i + 1), @@ -742,9 +749,11 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: i == 0 ? address(0) : tierConfigsRemaining[arrayIndex].reserveBeneficiary, encodedIPFSUri: tierConfigsRemaining[arrayIndex].encodedIPFSUri, category: tierConfigsRemaining[arrayIndex].category, + discountPercent: tierConfigsRemaining[arrayIndex].discountPercent, allowOwnerMint: tierConfigsRemaining[arrayIndex].allowOwnerMint, transfersPausable: tierConfigsRemaining[arrayIndex].transfersPausable, cannotBeRemoved: tierConfigsRemaining[arrayIndex].cannotBeRemoved, + cannotIncreaseDiscountPercent: tierConfigsRemaining[arrayIndex].cannotIncreaseDiscountPercent, resolvedUri: "" }); arrayIndex++; @@ -768,11 +777,13 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100 + i), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: true, - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); tiersAdded[i] = JB721Tier({ id: uint32(tiers.length + (i + 1)), @@ -784,9 +795,11 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: address(0), encodedIPFSUri: tierConfigsToAdd[i].encodedIPFSUri, category: tierConfigsToAdd[i].category, + discountPercent: tierConfigsToAdd[i].discountPercent, allowOwnerMint: tierConfigsToAdd[i].allowOwnerMint, transfersPausable: tierConfigsToAdd[i].transfersPausable, cannotBeRemoved: tierConfigsToAdd[i].cannotBeRemoved, + cannotIncreaseDiscountPercent: tierConfigsToAdd[i].cannotIncreaseDiscountPercent, resolvedUri: "" }); vm.expectEmit(true, true, true, true, address(hook)); @@ -835,11 +848,13 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: true, - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); tiers[i] = JB721Tier({ id: uint32(i + 1), @@ -851,9 +866,11 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: tierConfigs[i].reserveBeneficiary, encodedIPFSUri: tierConfigs[i].encodedIPFSUri, category: tierConfigs[i].category, + discountPercent: tierConfigs[i].discountPercent, allowOwnerMint: tierConfigs[i].allowOwnerMint, transfersPausable: tierConfigs[i].transfersPausable, cannotBeRemoved: tierConfigs[i].cannotBeRemoved, + cannotIncreaseDiscountPercent: tierConfigs[i].cannotIncreaseDiscountPercent, resolvedUri: "" }); } @@ -888,11 +905,13 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: true, - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); tiersAdded[i] = JB721Tier({ id: uint32(tiers.length + (i + 1)), @@ -904,13 +923,15 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: tierConfigsToAdd[i].reserveBeneficiary, encodedIPFSUri: tierConfigsToAdd[i].encodedIPFSUri, category: tierConfigsToAdd[i].category, + discountPercent: tierConfigsToAdd[i].discountPercent, allowOwnerMint: tierConfigsToAdd[i].allowOwnerMint, transfersPausable: tierConfigsToAdd[i].transfersPausable, cannotBeRemoved: tierConfigsToAdd[i].cannotBeRemoved, + cannotIncreaseDiscountPercent: tierConfigsToAdd[i].cannotIncreaseDiscountPercent, resolvedUri: "" }); } - vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.VOTING_UNITS_NOT_ALLOWED.selector)); + vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_VotingUnitsNotAllowed.selector)); vm.prank(owner); hook.adjustTiers(tierConfigsToAdd, new uint256[](0)); } @@ -936,11 +957,13 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: true, - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); tiers[i] = JB721Tier({ id: uint32(i + 1), @@ -952,9 +975,11 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: tierConfigs[i].reserveBeneficiary, encodedIPFSUri: tierConfigs[i].encodedIPFSUri, category: tierConfigs[i].category, + discountPercent: tierConfigs[i].discountPercent, allowOwnerMint: tierConfigs[i].allowOwnerMint, transfersPausable: tierConfigs[i].transfersPausable, cannotBeRemoved: tierConfigs[i].cannotBeRemoved, + cannotIncreaseDiscountPercent: tierConfigs[i].cannotIncreaseDiscountPercent, resolvedUri: "" }); } @@ -989,11 +1014,13 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: true, - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); tiersAdded[i] = JB721Tier({ id: uint32(tiers.length + (i + 1)), @@ -1005,15 +1032,19 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: tierConfigsToAdd[i].reserveBeneficiary, encodedIPFSUri: tierConfigsToAdd[i].encodedIPFSUri, category: tierConfigsToAdd[i].category, + discountPercent: tierConfigsToAdd[i].discountPercent, allowOwnerMint: tierConfigsToAdd[i].allowOwnerMint, transfersPausable: tierConfigsToAdd[i].transfersPausable, cannotBeRemoved: tierConfigsToAdd[i].cannotBeRemoved, + cannotIncreaseDiscountPercent: tierConfigsToAdd[i].cannotIncreaseDiscountPercent, resolvedUri: "" }); } // Expect the `adjustTiers` call to revert because of the `noNewTiersWithReserves` flag. - vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.RESERVE_FREQUENCY_NOT_ALLOWED.selector)); + vm.expectRevert( + abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_ReserveFrequencyNotAllowed.selector) + ); vm.prank(owner); hook.adjustTiers(tierConfigsToAdd, new uint256[](0)); } @@ -1033,11 +1064,13 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: true, - cannotBeRemoved: true + cannotBeRemoved: true, + cannotIncreaseDiscountPercent: false }); tierConfigs[1] = JB721TierConfig({ price: 10, @@ -1047,21 +1080,21 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: true, - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); // Deploy the hook and its store with the initial tiers. - JB721TiersHookStore store = new JB721TiersHookStore(); vm.etch(hook_i, address(hook).code); JB721TiersHook hook = JB721TiersHook(hook_i); hook.initialize( projectId, name, symbol, - IJBRulesets(mockJBRulesets), baseUri, IJB721TokenUriResolver(mockTokenUriResolver), contractUri, @@ -1071,7 +1104,6 @@ contract Test_adjustTier_Unit is UnitTestSetup { decimals: 18, prices: IJBPrices(address(0)) }), - IJB721TiersHookStore(address(store)), JB721TiersHookFlags({ preventOverspending: false, noNewTiersWithReserves: false, @@ -1082,7 +1114,7 @@ contract Test_adjustTier_Unit is UnitTestSetup { hook.transferOwnership(owner); // Expect the `adjustTiers` call to revert because cannot remove tier. - vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.CANT_REMOVE_TIER.selector)); + vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_CantRemoveTier.selector)); vm.prank(owner); hook.adjustTiers(new JB721TierConfig[](0), tierIdsToRemove); } @@ -1103,11 +1135,13 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: true, - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); tiers[i] = JB721Tier({ id: uint32(i + 1), @@ -1119,9 +1153,11 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: tierConfigs[i].reserveBeneficiary, encodedIPFSUri: tierConfigs[i].encodedIPFSUri, category: tierConfigs[i].category, + discountPercent: tierConfigs[i].discountPercent, allowOwnerMint: tierConfigs[i].allowOwnerMint, transfersPausable: tierConfigs[i].transfersPausable, cannotBeRemoved: tierConfigs[i].cannotBeRemoved, + cannotIncreaseDiscountPercent: tierConfigs[i].cannotIncreaseDiscountPercent, resolvedUri: "" }); } @@ -1156,11 +1192,13 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: false, - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); tiersAdded[i] = JB721Tier({ id: uint32(tiers.length + (i + 1)), @@ -1172,15 +1210,17 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: tierConfigsToAdd[i].reserveBeneficiary, encodedIPFSUri: tierConfigsToAdd[i].encodedIPFSUri, category: tierConfigsToAdd[i].category, + discountPercent: tierConfigsToAdd[i].discountPercent, allowOwnerMint: tierConfigsToAdd[i].allowOwnerMint, transfersPausable: tierConfigsToAdd[i].transfersPausable, cannotBeRemoved: tierConfigsToAdd[i].cannotBeRemoved, + cannotIncreaseDiscountPercent: tierConfigsToAdd[i].cannotIncreaseDiscountPercent, resolvedUri: "" }); } // Expect the `adjustTiers` call to revert because of the `initialSupply` of 0. - vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.NO_SUPPLY.selector)); + vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_NoSupply.selector)); vm.prank(owner); hook.adjustTiers(tierConfigsToAdd, new uint256[](0)); } @@ -1206,18 +1246,22 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, cannotBeRemoved: false, - useVotingUnits: true + useVotingUnits: true, + cannotIncreaseDiscountPercent: false }); } // Set the second to last tier to have a category of `99`, which is less than the last tier's category of `100`. tierConfigsToAdd[numberTiersToAdd - 1].category = uint8(99); // Expect the `adjustTiers` call to revert because of the invalid category sort order. - vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.INVALID_CATEGORY_SORT_ORDER.selector)); + vm.expectRevert( + abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InvalidCategorySortOrder.selector) + ); vm.prank(owner); hook.adjustTiers(tierConfigsToAdd, new uint256[](0)); } @@ -1242,11 +1286,13 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: false, - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); } @@ -1281,16 +1327,18 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: false, // <-- If false, voting power is based on tier price - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); } // Expect the `adjustTiers` call to revert because of the `noNewTiersWithVotes` flag. - vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.VOTING_UNITS_NOT_ALLOWED.selector)); + vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_VotingUnitsNotAllowed.selector)); vm.prank(owner); hook.adjustTiers(tierConfigsToAdd, new uint256[](0)); } @@ -1315,11 +1363,13 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: true, - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); } @@ -1354,11 +1404,13 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: true, // <-- If false, voting power is based on tier price - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); } @@ -1366,7 +1418,7 @@ contract Test_adjustTier_Unit is UnitTestSetup { tierConfigsToAdd[numberTiersToAdd - 1].votingUnits = uint16(1); // Expect the `adjustTiers` call to revert because of the `noNewTiersWithVotes` flag. - vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.VOTING_UNITS_NOT_ALLOWED.selector)); + vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_VotingUnitsNotAllowed.selector)); vm.prank(owner); hook.adjustTiers(tierConfigsToAdd, new uint256[](0)); } @@ -1419,11 +1471,13 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: false, - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); tiers[i] = JB721Tier({ id: uint32(i + 1), @@ -1435,9 +1489,11 @@ contract Test_adjustTier_Unit is UnitTestSetup { reserveBeneficiary: i == 0 ? address(0) : tierConfigs[i].reserveBeneficiary, encodedIPFSUri: tierConfigs[i].encodedIPFSUri, category: tierConfigs[i].category, + discountPercent: tierConfigs[i].discountPercent, allowOwnerMint: tierConfigs[i].allowOwnerMint, transfersPausable: tierConfigs[i].transfersPausable, cannotBeRemoved: tierConfigs[i].cannotBeRemoved, + cannotIncreaseDiscountPercent: tierConfigs[i].cannotIncreaseDiscountPercent, resolvedUri: "" }); } @@ -1523,4 +1579,138 @@ contract Test_adjustTier_Unit is UnitTestSetup { // Check: does the array have a length of 0? assertEq(intialTiers.length, 0, "Length mismatch."); } + + function test_setDiscountPercentOf_revertIfCannotIncreaseDiscount() public { + // Initial tier config and data. + JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1); + tierConfigs[0] = JB721TierConfig({ + price: 10, + initialSupply: uint32(100), + votingUnits: uint16(0), + reserveFrequency: uint16(0), + reserveBeneficiary: reserveBeneficiary, + encodedIPFSUri: tokenUris[0], + category: uint24(100), + discountPercent: uint8(0), + allowOwnerMint: false, + useReserveBeneficiaryAsDefault: false, + transfersPausable: false, + useVotingUnits: true, + cannotBeRemoved: true, + cannotIncreaseDiscountPercent: true + }); + // Deploy the hook and its store with the initial tiers. + vm.etch(hook_i, address(hook).code); + JB721TiersHook hook = JB721TiersHook(hook_i); + hook.initialize( + projectId, + name, + symbol, + baseUri, + IJB721TokenUriResolver(mockTokenUriResolver), + contractUri, + JB721InitTiersConfig({ + tiers: tierConfigs, + currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), + decimals: 18, + prices: IJBPrices(address(0)) + }), + JB721TiersHookFlags({ + preventOverspending: false, + noNewTiersWithReserves: false, + noNewTiersWithVotes: false, + noNewTiersWithOwnerMinting: true + }) + ); + hook.transferOwnership(owner); + + // Expect the `setDiscountPercentOf` call to revert because of the flag. + vm.expectRevert( + abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_DiscountPercentIncreaseNotAllowed.selector) + ); + vm.prank(owner); + // Attempt to increase the discount of the first tier to 100% + hook.setDiscountPercentOf(1, 100); + } + + function test_setDiscountPercentsOf_revertIfCannotIncreaseDiscounts() public { + // Initial tier config and data. + JB721TierConfig[] memory initialConfig = new JB721TierConfig[](2); + initialConfig[0] = JB721TierConfig({ + price: 10, + initialSupply: uint32(100), + votingUnits: uint16(0), + reserveFrequency: uint16(0), + reserveBeneficiary: reserveBeneficiary, + encodedIPFSUri: tokenUris[0], + category: uint24(100), + discountPercent: uint8(0), + allowOwnerMint: false, + useReserveBeneficiaryAsDefault: false, + transfersPausable: false, + useVotingUnits: true, + cannotBeRemoved: true, + cannotIncreaseDiscountPercent: true + }); + initialConfig[1] = JB721TierConfig({ + price: 10, + initialSupply: uint32(100), + votingUnits: uint16(0), + reserveFrequency: uint16(0), + reserveBeneficiary: reserveBeneficiary, + encodedIPFSUri: tokenUris[0], + category: uint24(100), + discountPercent: uint8(0), + allowOwnerMint: false, + useReserveBeneficiaryAsDefault: false, + transfersPausable: false, + useVotingUnits: true, + cannotBeRemoved: true, + cannotIncreaseDiscountPercent: false + }); + + // Deploy the hook and its store with the initial tiers. + vm.etch(hook_i, address(hook).code); + JB721TiersHook hook = JB721TiersHook(hook_i); + hook.initialize( + projectId, + name, + symbol, + baseUri, + IJB721TokenUriResolver(mockTokenUriResolver), + contractUri, + JB721InitTiersConfig({ + tiers: initialConfig, + currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), + decimals: 18, + prices: IJBPrices(address(0)) + }), + JB721TiersHookFlags({ + preventOverspending: false, + noNewTiersWithReserves: false, + noNewTiersWithVotes: false, + noNewTiersWithOwnerMinting: true + }) + ); + hook.transferOwnership(owner); + + // Build calldata for increasing multiple tier discounts at once. + JB721TiersSetDiscountPercentConfig[] memory discountCalldata = new JB721TiersSetDiscountPercentConfig[](2); + discountCalldata[0] = JB721TiersSetDiscountPercentConfig({ + tierId: 1, + discountPercent: 100 // invalid + }); + + discountCalldata[1] = JB721TiersSetDiscountPercentConfig({ + tierId: 2, + discountPercent: 100 // valid + }); + + // Expect the `setDiscountPercentsOf` call to revert because of the flag. + vm.expectRevert( + abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_DiscountPercentIncreaseNotAllowed.selector) + ); + vm.prank(owner); + hook.setDiscountPercentsOf(discountCalldata); + } } diff --git a/test/unit/getters_constructor_Unit.t.sol b/test/unit/getters_constructor_Unit.t.sol index 37bd2a80..61ee2f08 100644 --- a/test/unit/getters_constructor_Unit.t.sol +++ b/test/unit/getters_constructor_Unit.t.sol @@ -22,7 +22,6 @@ contract Test_Getters_Constructor_Unit is UnitTestSetup { JBDeploy721TiersHookConfig memory hookConfig = JBDeploy721TiersHookConfig( name, symbol, - IJBRulesets(mockJBRulesets), baseUri, IJB721TokenUriResolver(mockTokenUriResolver), contractUri, @@ -45,15 +44,16 @@ contract Test_Getters_Constructor_Unit is UnitTestSetup { assertEq(address(prices2), prices); } - function test_bools_doesPackingAndUnpackingWork(bool a, bool b, bool c, bool d) public { + function test_bools_doesPackingAndUnpackingWork(bool a, bool b, bool c, bool d, bool e) public { ForTest_JB721TiersHookStore store = new ForTest_JB721TiersHookStore(); - uint8 packed = store.ForTest_packBools(a, b, c, d); - (bool a2, bool b2, bool c2, bool d2) = store.ForTest_unpackBools(packed); + uint8 packed = store.ForTest_packBools(a, b, c, d, e); + (bool a2, bool b2, bool c2, bool d2, bool e2) = store.ForTest_unpackBools(packed); // Check: do the packed values match the unpacked values? assertEq(a, a2); assertEq(b, b2); assertEq(c, c2); assertEq(d, d2); + assertEq(e, e2); } function test_tiersOf_returnsAllTiersWithResolver(uint256 numberOfTiers) public { @@ -146,9 +146,11 @@ contract Test_Getters_Constructor_Unit is UnitTestSetup { reserveBeneficiary: address(0), encodedIPFSUri: bytes32(0), category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, transfersPausable: false, cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false, resolvedUri: "" }) ); @@ -174,7 +176,8 @@ contract Test_Getters_Constructor_Unit is UnitTestSetup { votingUnits: uint16(0), reserveFrequency: uint16(0), category: uint24(100), - packedBools: hook.test_store().ForTest_packBools(false, false, false, false) + discountPercent: uint8(0), + packedBools: hook.test_store().ForTest_packBools(false, false, false, false, false) }) ); } @@ -225,7 +228,8 @@ contract Test_Getters_Constructor_Unit is UnitTestSetup { votingUnits: uint16(0), reserveFrequency: uint16(reserveFrequency), category: uint24(100), - packedBools: hook.test_store().ForTest_packBools(false, false, false, false) + discountPercent: uint8(0), + packedBools: hook.test_store().ForTest_packBools(false, false, false, false, false) }) ); // Manually set the number of reserve mints for each tier. @@ -264,7 +268,8 @@ contract Test_Getters_Constructor_Unit is UnitTestSetup { votingUnits: uint16(0), reserveFrequency: uint16(100), category: uint24(100), - packedBools: hook.test_store().ForTest_packBools(false, false, true, false) + discountPercent: uint8(0), + packedBools: hook.test_store().ForTest_packBools(false, false, true, false, false) }) ); @@ -386,7 +391,8 @@ contract Test_Getters_Constructor_Unit is UnitTestSetup { votingUnits: uint16(0), reserveFrequency: uint16(0), category: uint24(100), - packedBools: hook.test_store().ForTest_packBools(false, false, false, false) + discountPercent: uint8(0), + packedBools: hook.test_store().ForTest_packBools(false, false, false, false, false) }) ); // Calculate the theoretical weight for the current tier. 10 the price multiplier. @@ -481,27 +487,27 @@ contract Test_Getters_Constructor_Unit is UnitTestSetup { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[0], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: true, - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); } // Set the initial supply of the tier at `errorIndex` to 0. This should cause an error. tiers[errorIndex].initialSupply = 0; - JB721TiersHookStore store = new JB721TiersHookStore(); // Expect the error. - vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.NO_SUPPLY.selector)); + vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_NoSupply.selector)); vm.etch(hook_i, address(hook).code); JB721TiersHook hook = JB721TiersHook(hook_i); hook.initialize( projectId, name, symbol, - IJBRulesets(mockJBRulesets), baseUri, IJB721TokenUriResolver(mockTokenUriResolver), contractUri, @@ -511,7 +517,6 @@ contract Test_Getters_Constructor_Unit is UnitTestSetup { decimals: 18, prices: IJBPrices(address(0)) }), - store, JB721TiersHookFlags({ preventOverspending: false, noNewTiersWithReserves: true, diff --git a/test/unit/mintFor_mintReservesFor_Unit.t.sol b/test/unit/mintFor_mintReservesFor_Unit.t.sol index 35ca6f1f..de3bb471 100644 --- a/test/unit/mintFor_mintReservesFor_Unit.t.sol +++ b/test/unit/mintFor_mintReservesFor_Unit.t.sol @@ -31,7 +31,8 @@ contract Test_mintFor_mintReservesFor_Unit is UnitTestSetup { votingUnits: uint16(0), reserveFrequency: uint16(reserveFrequency), category: uint24(100), - packedBools: hook.test_store().ForTest_packBools(false, false, true, false) + discountPercent: uint8(0), + packedBools: hook.test_store().ForTest_packBools(false, false, true, false, false) }) ); hook.test_store().ForTest_setReservesMintedFor(address(hook), i + 1, reservedMinted); @@ -81,7 +82,8 @@ contract Test_mintFor_mintReservesFor_Unit is UnitTestSetup { votingUnits: uint16(0), reserveFrequency: uint16(reserveFrequency), category: uint24(100), - packedBools: hook.test_store().ForTest_packBools(false, false, true, false) + discountPercent: uint8(0), + packedBools: hook.test_store().ForTest_packBools(false, false, true, false, false) }) ); @@ -151,13 +153,14 @@ contract Test_mintFor_mintReservesFor_Unit is UnitTestSetup { allowSetController: false, allowAddAccountingContext: false, allowAddPriceFeed: false, + allowCrosschainSuckerExtension: false, ownerMustSendPayouts: false, holdFees: false, useTotalSurplusForRedemptions: false, useDataHookForPay: true, useDataHookForRedeem: true, dataHook: address(0), - metadata: 8 // the first 2 bits are discarded, so this is 010. + metadata: 16 // the first 3 bits are discarded, so this is 0100. }) ) }) @@ -177,7 +180,8 @@ contract Test_mintFor_mintReservesFor_Unit is UnitTestSetup { votingUnits: uint16(0), reserveFrequency: uint16(reserveFrequency), category: uint24(100), - packedBools: hook.test_store().ForTest_packBools(false, false, true, false) + discountPercent: uint8(0), + packedBools: hook.test_store().ForTest_packBools(false, false, true, false, false) }) ); hook.test_store().ForTest_setReservesMintedFor(address(hook), i + 1, reservedMinted); @@ -188,7 +192,7 @@ contract Test_mintFor_mintReservesFor_Unit is UnitTestSetup { for (uint256 tier = 1; tier <= numberOfTiers; tier++) { uint256 mintable = hook.test_store().numberOfPendingReservesFor(address(hook), tier); vm.prank(owner); - vm.expectRevert(JB721TiersHook.MINT_RESERVE_NFTS_PAUSED.selector); + vm.expectRevert(JB721TiersHook.JB721TiersHook_MintReserveNftsPaused.selector); hook.mintPendingReservesFor(tier, mintable); } } @@ -213,7 +217,8 @@ contract Test_mintFor_mintReservesFor_Unit is UnitTestSetup { votingUnits: uint16(0), reserveFrequency: uint16(reserveFrequency), category: uint24(100), - packedBools: hook.test_store().ForTest_packBools(false, false, true, false) + discountPercent: uint8(0), + packedBools: hook.test_store().ForTest_packBools(false, false, true, false, false) }) ); hook.test_store().ForTest_setReservesMintedFor(address(hook), i + 1, reservedMinted); @@ -226,7 +231,9 @@ contract Test_mintFor_mintReservesFor_Unit is UnitTestSetup { // Increase it by 1 to cause an error, then attempt to mint. amount++; // Check: is the correct error thrown? - vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.INSUFFICIENT_PENDING_RESERVES.selector)); + vm.expectRevert( + abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientPendingReserves.selector) + ); vm.prank(owner); hook.mintPendingReservesFor(i, amount); } @@ -255,7 +262,8 @@ contract Test_mintFor_mintReservesFor_Unit is UnitTestSetup { votingUnits: uint16(0), reserveFrequency: uint16(reserveFrequency), category: uint24(100), - packedBools: hook.test_store().ForTest_packBools(false, false, true, false) + discountPercent: uint8(0), + packedBools: hook.test_store().ForTest_packBools(false, false, true, false, false) }) ); hook.test_store().ForTest_setReservesMintedFor(address(hook), i + 1, reservedMinted); @@ -323,7 +331,7 @@ contract Test_mintFor_mintReservesFor_Unit is UnitTestSetup { vm.prank(owner); // Expect the function call to revert with the specified error message. - vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.CANT_MINT_MANUALLY.selector)); + vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_CantMintManually.selector)); // Call the `mintFor` function to trigger the revert. hook.mintFor(tiersToMint, beneficiary); diff --git a/test/unit/pay_Unit.t.sol b/test/unit/pay_Unit.t.sol index bf7833a9..72b41c4e 100644 --- a/test/unit/pay_Unit.t.sol +++ b/test/unit/pay_Unit.t.sol @@ -107,7 +107,7 @@ contract Test_afterPayRecorded_Unit is UnitTestSetup { ); // Expect a revert for overspending. - vm.expectRevert(abi.encodeWithSelector(JB721TiersHook.OVERSPENDING.selector)); + vm.expectRevert(abi.encodeWithSelector(JB721TiersHook.JB721TiersHook_Overspending.selector)); vm.prank(mockTerminalAddress); hook.afterPayRecordedWith( @@ -707,7 +707,7 @@ contract Test_afterPayRecorded_Unit is UnitTestSetup { vm.prank(owner); hook.adjustTiers(new JB721TierConfig[](0), toRemove); - vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.TIER_REMOVED.selector)); + vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_TierRemoved.selector)); vm.prank(mockTerminalAddress); hook.afterPayRecordedWith( @@ -773,7 +773,7 @@ contract Test_afterPayRecorded_Unit is UnitTestSetup { vm.prank(owner); hook.adjustTiers(new JB721TierConfig[](0), toRemove); - vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.INVALID_TIER.selector)); + vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InvalidTier.selector)); vm.prank(mockTerminalAddress); hook.afterPayRecordedWith( @@ -835,7 +835,7 @@ contract Test_afterPayRecorded_Unit is UnitTestSetup { bytes memory hookMetadata = metadataHelper.createMetadata(ids, data); // Expect a revert for the amount being too low. - vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.PRICE_EXCEEDS_AMOUNT.selector)); + vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_PriceExceedsAmount.selector)); vm.prank(mockTerminalAddress); hook.afterPayRecordedWith( @@ -899,7 +899,9 @@ contract Test_afterPayRecorded_Unit is UnitTestSetup { // If there is no remaining supply, this should revert. if (supplyLeft == 0) { - vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.INSUFFICIENT_SUPPLY_REMAINING.selector)); + vm.expectRevert( + abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientSupplyRemaining.selector) + ); } // Execute the payment. @@ -955,7 +957,7 @@ contract Test_afterPayRecorded_Unit is UnitTestSetup { vm.prank(terminal); // Expect a revert for the caller not being a terminal of the project. - vm.expectRevert(abi.encodeWithSelector(JB721Hook.INVALID_PAY.selector)); + vm.expectRevert(abi.encodeWithSelector(JB721Hook.JB721Hook_InvalidPay.selector)); hook.afterPayRecordedWith( JBAfterPayRecordedContext({ @@ -1155,7 +1157,7 @@ contract Test_afterPayRecorded_Unit is UnitTestSetup { // Generate the metadata. bytes memory hookMetadata = metadataHelper.createMetadata(ids, data); vm.prank(mockTerminalAddress); - vm.expectRevert(abi.encodeWithSelector(JB721TiersHook.OVERSPENDING.selector)); + vm.expectRevert(abi.encodeWithSelector(JB721TiersHook.JB721TiersHook_Overspending.selector)); hook.afterPayRecordedWith( JBAfterPayRecordedContext({ payer: msg.sender, @@ -1215,7 +1217,7 @@ contract Test_afterPayRecorded_Unit is UnitTestSetup { // If prevent is enabled the call should revert. Otherwise, we should receive pay credits. if (prevent) { - vm.expectRevert(abi.encodeWithSelector(JB721TiersHook.OVERSPENDING.selector)); + vm.expectRevert(abi.encodeWithSelector(JB721TiersHook.JB721TiersHook_Overspending.selector)); } else { uint256 payCredits = hook.payCreditsOf(beneficiary); uint256 stashedPayCredits = payCredits; @@ -1293,13 +1295,14 @@ contract Test_afterPayRecorded_Unit is UnitTestSetup { allowSetController: false, allowAddAccountingContext: false, allowAddPriceFeed: false, + allowCrosschainSuckerExtension: false, ownerMustSendPayouts: false, holdFees: false, useTotalSurplusForRedemptions: false, useDataHookForPay: true, useDataHookForRedeem: true, dataHook: address(hook), - metadata: 4 // the first 2 bits are discarded, so this is 001. + metadata: 8 // the first 3 bits are discarded, so this is 0001. }) ) }) @@ -1353,7 +1356,7 @@ contract Test_afterPayRecorded_Unit is UnitTestSetup { uint256 tokenId = _generateTokenId(1, 1); // Expect a revert on account of transfers being paused. - vm.expectRevert(JB721TiersHook.TIER_TRANSFERS_PAUSED.selector); + vm.expectRevert(JB721TiersHook.JB721TiersHook_TierTransfersPaused.selector); vm.prank(msg.sender); IERC721(hook).transferFrom(msg.sender, beneficiary, tokenId); diff --git a/test/unit/redeem_Unit.t.sol b/test/unit/redeem_Unit.t.sol index 624e680a..55868437 100644 --- a/test/unit/redeem_Unit.t.sol +++ b/test/unit/redeem_Unit.t.sol @@ -23,7 +23,8 @@ contract Test_redeem_Unit is UnitTestSetup { votingUnits: uint16(0), reserveFrequency: uint16(0), category: uint24(100), - packedBools: hook.test_store().ForTest_packBools(false, false, false, false) + discountPercent: uint8(0), + packedBools: hook.test_store().ForTest_packBools(false, false, false, false, false) }) ); totalWeight += (10 * i - 5 * i) * i * 10; @@ -95,7 +96,8 @@ contract Test_redeem_Unit is UnitTestSetup { votingUnits: uint16(0), reserveFrequency: uint16(0), category: uint24(100), - packedBools: hook.test_store().ForTest_packBools(false, false, false, false) + discountPercent: uint8(0), + packedBools: hook.test_store().ForTest_packBools(false, false, false, false, false) }) ); totalWeight += (10 * i - 5 * i) * i * 10; @@ -153,7 +155,8 @@ contract Test_redeem_Unit is UnitTestSetup { votingUnits: uint16(0), reserveFrequency: uint16(0), category: uint24(100), - packedBools: hook.test_store().ForTest_packBools(false, false, false, false) + discountPercent: uint8(0), + packedBools: hook.test_store().ForTest_packBools(false, false, false, false, false) }) ); totalWeight += (10 * i - 5 * i) * i * 10; @@ -209,7 +212,7 @@ contract Test_redeem_Unit is UnitTestSetup { vm.assume(tokenCount > 0); // Expect a revert on account of the token count being non-zero while the total supply is zero. - vm.expectRevert(abi.encodeWithSelector(JB721Hook.UNEXPECTED_TOKEN_REDEEMED.selector)); + vm.expectRevert(abi.encodeWithSelector(JB721Hook.JB721Hook_UnexpectedTokenRedeemed.selector)); hook.beforeRedeemRecordedWith( JBBeforeRedeemRecordedContext({ @@ -359,7 +362,7 @@ contract Test_redeem_Unit is UnitTestSetup { ); // Expect to revert on account of the project ID being incorrect. - vm.expectRevert(abi.encodeWithSelector(JB721Hook.INVALID_REDEEM.selector)); + vm.expectRevert(abi.encodeWithSelector(JB721Hook.JB721Hook_InvalidRedeem.selector)); vm.prank(mockTerminalAddress); hook.afterRedeemRecordedWith( @@ -400,7 +403,7 @@ contract Test_redeem_Unit is UnitTestSetup { ); // Expect to revert on account of the caller not being a terminal of the project. - vm.expectRevert(abi.encodeWithSelector(JB721Hook.INVALID_REDEEM.selector)); + vm.expectRevert(abi.encodeWithSelector(JB721Hook.JB721Hook_InvalidRedeem.selector)); vm.prank(mockTerminalAddress); hook.afterRedeemRecordedWith( @@ -458,7 +461,7 @@ contract Test_redeem_Unit is UnitTestSetup { abi.encode(true) ); - vm.expectRevert(abi.encodeWithSelector(JB721Hook.UNAUTHORIZED_TOKEN.selector, tokenId)); + vm.expectRevert(abi.encodeWithSelector(JB721Hook.JB721Hook_UnauthorizedToken.selector)); vm.prank(mockTerminalAddress); hook.afterRedeemRecordedWith( diff --git a/test/utils/ForTest_JB721TiersHook.sol b/test/utils/ForTest_JB721TiersHook.sol index 4e4e6264..c2012b23 100644 --- a/test/utils/ForTest_JB721TiersHook.sol +++ b/test/utils/ForTest_JB721TiersHook.sol @@ -26,7 +26,8 @@ interface IJB721TiersHookStore_ForTest is IJB721TiersHookStore { bool allowOwnerMint, bool transfersPausable, bool useVotingUnits, - bool cannotBeRemoved + bool cannotBeRemoved, + bool cannotIncreaseDiscountPercent ) external returns (uint8); @@ -55,14 +56,13 @@ contract ForTest_JB721TiersHook is JB721TiersHook { JB721TiersHookFlags memory flags ) // The directory is also `IJBPermissioned`. - JB721TiersHook(directory, IJBPermissioned(address(directory)).PERMISSIONS(), _trustedForwarder) + JB721TiersHook(directory, IJBPermissioned(address(directory)).PERMISSIONS(), rulesets, store, _trustedForwarder) { // Disable the safety check to not allow initializing the original contract JB721TiersHook.initialize( projectId, name, symbol, - rulesets, baseUri, tokenUriResolver, contractUri, @@ -72,7 +72,6 @@ contract ForTest_JB721TiersHook is JB721TiersHook { decimals: 18, prices: IJBPrices(address(0)) }), - store, flags ); test_store = IJB721TiersHookStore_ForTest(address(store)); @@ -106,7 +105,7 @@ contract ForTest_JB721TiersHookStore is JB721TiersHookStore, IJB721TiersHookStor storedTier = _storedTierOf[nft][currentSortIndex]; // Unpack stored tier. - (bool allowOwnerMint, bool transfersPausable,,) = _unpackBools(storedTier.packedBools); + (bool allowOwnerMint, bool transfersPausable,,,) = _unpackBools(storedTier.packedBools); // Add the tier to the array being returned. tiers[numberOfIncludedTiers++] = JB721Tier({ @@ -119,9 +118,11 @@ contract ForTest_JB721TiersHookStore is JB721TiersHookStore, IJB721TiersHookStor reserveBeneficiary: reserveBeneficiaryOf(nft, currentSortIndex), encodedIPFSUri: encodedIPFSUriOf[nft][currentSortIndex], category: storedTier.category, + discountPercent: storedTier.discountPercent, allowOwnerMint: allowOwnerMint, transfersPausable: transfersPausable, cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false, resolvedUri: "" }); // Set the next sort index. @@ -160,16 +161,19 @@ contract ForTest_JB721TiersHookStore is JB721TiersHookStore, IJB721TiersHookStor bool allowOwnerMint, bool transfersPausable, bool useVotingUnits, - bool cannotBeRemoved + bool cannotBeRemoved, + bool cannotIncreaseDiscountPercent ) public pure returns (uint8) { - return _packBools(allowOwnerMint, transfersPausable, useVotingUnits, cannotBeRemoved); + return _packBools( + allowOwnerMint, transfersPausable, useVotingUnits, cannotBeRemoved, cannotIncreaseDiscountPercent + ); } - function ForTest_unpackBools(uint8 packed) public pure returns (bool, bool, bool, bool) { + function ForTest_unpackBools(uint8 packed) public pure returns (bool, bool, bool, bool, bool) { return _unpackBools(packed); } } diff --git a/test/utils/TestBaseWorkflow.sol b/test/utils/TestBaseWorkflow.sol index caf4f6d9..43c939be 100644 --- a/test/utils/TestBaseWorkflow.sol +++ b/test/utils/TestBaseWorkflow.sol @@ -86,7 +86,7 @@ contract TestBaseWorkflow is Test { jbDirectory = new JBDirectory(jbPermissions, jbProjects, projectOwner); vm.label(address(jbDirectory), "JBDirectory"); - jbPrices = new JBPrices(jbPermissions, jbProjects, jbDirectory, projectOwner); + jbPrices = new JBPrices(jbDirectory, jbPermissions, jbProjects, projectOwner); vm.label(address(jbPrices), "JBPrices"); jbRulesets = new JBRulesets(jbDirectory); @@ -105,14 +105,14 @@ contract TestBaseWorkflow is Test { vm.label(address(jbSplits), "JBSplits"); jbController = new JBController( + jbDirectory, + jbFundAccessLimits, jbPermissions, + jbPrices, jbProjects, - jbDirectory, jbRulesets, - jbTokens, jbSplits, - jbFundAccessLimits, - jbPrices, + jbTokens, address(0) ); vm.label(address(jbController), "JBController"); @@ -120,13 +120,13 @@ contract TestBaseWorkflow is Test { vm.prank(projectOwner); jbDirectory.setIsAllowedToSetFirstController(address(jbController), true); - jbTerminalStore = new JBTerminalStore(jbDirectory, jbRulesets, jbPrices); + jbTerminalStore = new JBTerminalStore(jbDirectory, jbPrices, jbRulesets); vm.label(address(jbTerminalStore), "JBTerminalStore"); accessJBLib = new AccessJBLib(); jbMultiTerminal = new JBMultiTerminal( - jbPermissions, jbProjects, jbSplits, jbTerminalStore, jbFeelessAddresses, IPermit2(address(0)), address(0) + jbFeelessAddresses, jbPermissions, jbProjects, jbSplits, jbTerminalStore, IPermit2(address(0)), address(0) ); vm.label(address(jbMultiTerminal), "JBMultiTerminal"); diff --git a/test/utils/UnitTestSetup.sol b/test/utils/UnitTestSetup.sol index 996fffca..66cab5b7 100644 --- a/test/utils/UnitTestSetup.sol +++ b/test/utils/UnitTestSetup.sol @@ -140,10 +140,12 @@ contract UnitTestSetup is Test { reserveBeneficiary: reserveBeneficiary, // Use default beneficiary. encodedIPFSUri: bytes32(0), // Use default hashes array. category: type(uint24).max, + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false, useVotingUnits: true }); @@ -158,11 +160,13 @@ contract UnitTestSetup is Test { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[i], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: true, - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }) ); } @@ -193,6 +197,7 @@ contract UnitTestSetup is Test { allowSetController: false, allowAddAccountingContext: false, allowAddPriceFeed: false, + allowCrosschainSuckerExtension: false, ownerMustSendPayouts: false, holdFees: false, useTotalSurplusForRedemptions: false, @@ -211,15 +216,19 @@ contract UnitTestSetup is Test { mockJBDirectory, abi.encodeWithSelector(IJBPermissioned.PERMISSIONS.selector), abi.encode(mockJBPermissions) ); - hookOrigin = - new JB721TiersHook(IJBDirectory(mockJBDirectory), IJBPermissions(mockJBPermissions), trustedForwarder); - addressRegistry = new JBAddressRegistry(); store = new JB721TiersHookStore(); + hookOrigin = new JB721TiersHook( + IJBDirectory(mockJBDirectory), + IJBPermissions(mockJBPermissions), + IJBRulesets(mockJBRulesets), + IJB721TiersHookStore(store), + trustedForwarder + ); + addressRegistry = new JBAddressRegistry(); jbHookDeployer = new JB721TiersHookDeployer(hookOrigin, store, addressRegistry, trustedForwarder); JBDeploy721TiersHookConfig memory hookConfig = JBDeploy721TiersHookConfig( name, symbol, - IJBRulesets(mockJBRulesets), baseUri, IJB721TokenUriResolver(mockTokenUriResolver), contractUri, @@ -481,11 +490,13 @@ contract UnitTestSetup is Test { category: categoryIncrement == 0 ? tierConfig.category == type(uint24).max ? uint24(i * 2 + 1) : tierConfig.category : uint24(i * 2 + categoryIncrement), + discountPercent: tierConfig.discountPercent, allowOwnerMint: tierConfig.allowOwnerMint, useReserveBeneficiaryAsDefault: tierConfig.useReserveBeneficiaryAsDefault, transfersPausable: tierConfig.transfersPausable, useVotingUnits: tierConfig.useVotingUnits, - cannotBeRemoved: tierConfig.cannotBeRemoved + cannotBeRemoved: tierConfig.cannotBeRemoved, + cannotIncreaseDiscountPercent: tierConfig.cannotIncreaseDiscountPercent }); newTiers[i] = JB721Tier({ @@ -498,9 +509,11 @@ contract UnitTestSetup is Test { reserveBeneficiary: tierConfigs[i].reserveBeneficiary, encodedIPFSUri: tierConfigs[i].encodedIPFSUri, category: tierConfigs[i].category, + discountPercent: tierConfigs[i].discountPercent, allowOwnerMint: tierConfigs[i].allowOwnerMint, transfersPausable: tierConfigs[i].transfersPausable, cannotBeRemoved: tierConfigs[i].cannotBeRemoved, + cannotIncreaseDiscountPercent: tierConfigs[i].cannotIncreaseDiscountPercent, resolvedUri: defaultTierConfig.encodedIPFSUri == bytes32(0) ? "" : string(abi.encodePacked("resolverURI", _generateTokenId(initialId + i + 1, 0))) @@ -581,9 +594,6 @@ contract UnitTestSetup is Test { vm.etch(hook_i, address(hook).code); tiersHook = JB721TiersHook(hook_i); - // Deploy the hook store. - JB721TiersHookStore hookStore = new JB721TiersHookStore(); - // Initialize the hook's flags and init config in memory (for stack's sake). JB721TiersHookFlags memory flags = JB721TiersHookFlags({ preventOverspending: preventOverspending, @@ -603,12 +613,10 @@ contract UnitTestSetup is Test { projectId, name, symbol, - IJBRulesets(mockJBRulesets), baseUri, IJB721TokenUriResolver(mockTokenUriResolver), contractUri, initConfig, - IJB721TiersHookStore(hookStore), flags ); @@ -670,17 +678,18 @@ contract UnitTestSetup is Test { reserveBeneficiary: reserveBeneficiary, encodedIPFSUri: tokenUris[i], category: uint24(100), + discountPercent: uint8(0), allowOwnerMint: false, useReserveBeneficiaryAsDefault: false, transfersPausable: false, useVotingUnits: true, - cannotBeRemoved: false + cannotBeRemoved: false, + cannotIncreaseDiscountPercent: false }); } tiersHookConfig = JBDeploy721TiersHookConfig({ name: name, symbol: symbol, - rulesets: IJBRulesets(mockJBRulesets), baseUri: baseUri, tokenUriResolver: IJB721TokenUriResolver(mockTokenUriResolver), contractUri: contractUri, @@ -714,6 +723,7 @@ contract UnitTestSetup is Test { ownerMustSendPayouts: false, allowAddAccountingContext: false, allowAddPriceFeed: false, + allowCrosschainSuckerExtension: false, holdFees: false, useTotalSurplusForRedemptions: false, useDataHookForRedeem: false,