diff --git a/contracts/interfaces/IPolicy.sol b/contracts/interfaces/IPolicy.sol index 7d5f83d..b2a01c6 100644 --- a/contracts/interfaces/IPolicy.sol +++ b/contracts/interfaces/IPolicy.sol @@ -8,10 +8,14 @@ interface IPolicy { /// @notice Returns the string identifier associated with the policy. function name() external pure returns (string memory); + /// @notice Returns the business/strategy model implemented by the policy. + /// @return A detailed description of the subscription policy as bytes. + function description() external pure override returns (bytes memory); + /// @notice Retrieves the access terms for a specific account and content ID. /// @param account The address of the account for which access terms are being retrieved. /// @param contentId The ID of the content associated with the access terms. - /// @return The access terms as a `bytes` array, which can contain any necessary data + /// @return The access terms as a `bytes` array, which can contain any necessary data /// for validating on-chain or off-chain access. eg: PILTerms https://docs.story.foundation/docs/pil-terms function terms( address account, @@ -30,7 +34,7 @@ interface IPolicy { /// @dev This method is expected to be called only by RM contract and its used to establish /// any logic related to access, validations, etc... /// @param deal The deal object containing the terms agreed upon between the content holder and the account. - /// @param data Additional data required for processing the deal. + /// @param data Additional data required for processing the deal. /// @return bool A boolean indicating whether the deal was successfully executed (`true`) or not (`false`). /// @return string A message providing context for the execution result. function exec( diff --git a/examples/GatedPolicy.sol b/examples/GatedPolicy.sol new file mode 100644 index 0000000..56a7b80 --- /dev/null +++ b/examples/GatedPolicy.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "contracts/base/BasePolicy.sol"; +import "contracts/interfaces/IPolicy.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; // For NFT gating + +/// @title GatedContentPolicy +/// @notice Implements a content access policy where users must meet specific criteria to access the gated content. +contract GatedContentPolicy is BasePolicy, IPolicy { + address public nftToken; // Address of the NFT token used for gating access. + mapping(address => mapping(uint256 => bool)) public accessList; // Tracks access by contentId. + + /// @notice Constructor for the GatedContentPolicy contract. + /// @param rmAddress Address of the Rights Manager (RM) contract. + /// @param ownershipAddress Address of the Ownership contract. + /// @param nftTokenAddress Address of the NFT token contract used for gating. + constructor( + address rmAddress, + address ownershipAddress, + address nftTokenAddress + ) BasePolicy(rmAddress, ownershipAddress) { + nftToken = nftTokenAddress; + } + + /// @notice Register a user on the whitelist for specific content. + /// @param user The address of the user to be whitelisted. + /// @param contentId The ID of the content. + function addToWhitelist(address user, uint256 contentId) external onlyOwner { + accessList[user][contentId] = true; + } + + /// @notice Check whether a user meets the access criteria for a specific content. + /// @param account The address of the account to check. + /// @param contentId The ID of the content to check. + /// @return bool Returns true if the user has access to the content. + function comply( + address account, + uint256 contentId + ) external view override returns (bool) { + bool ownsNFT = IERC721(nftToken).balanceOf(account) > 0; + bool isWhitelisted = accessList[account][contentId]; + + return ownsNFT || isWhitelisted; + } + + /// @notice Execute the logic of access validation. + /// @param deal The deal object containing the terms agreed upon between the content holder and the user. + /// @param data Additional data needed for processing the deal. + /// @return bool A boolean indicating whether the execution was successful. + /// @return string A message providing context for the execution result. + function exec( + T.Deal calldata deal, + bytes calldata data + ) external onlyRM returns (bool, string memory) { + if (comply(deal.account, deal.contentId)) { + return (true, "Access granted"); + } else { + return (false, "Access denied"); + } + } + + /// @notice Returns a detailed description of the gated content policy. + function description() external pure override returns (bytes memory) { + return abi.encodePacked( + "The GatedContentPolicy restricts access to content based on user criteria such as owning a specific NFT, ", + "being whitelisted by the content holder, or paying an access fee. Users must fulfill at least one of these criteria ", + "to access the gated content."; + ) + } + +} diff --git a/examples/RentalPolicy.sol b/examples/RentalPolicy.sol index 9a10b57..978bc92 100644 --- a/examples/RentalPolicy.sol +++ b/examples/RentalPolicy.sol @@ -1,30 +1,66 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; + import "contracts/base/BasePolicy.sol"; import "contracts/interfaces/IPolicy.sol"; import "contracts/libraries/Types.sol"; import "contracts/libraries/TreasuryHelper.sol"; +/// @title RentalPolicy +/// @notice This contract implements the IPolicy interface to manage content rental terms. +/// It allows for registering content with rental durations and prices and handles the rental process. contract RentalPolicy is BasePolicy, IPolicy { using TreasuryHelper for address; + /// @dev Structure to hold rental details for content. struct Content { - uint256 rentalDuration; - uint256 price; + uint256 rentalDuration; // Duration in seconds for which content is rented. + uint256 price; // Price to rent the content. } + // Mapping to store content data by content ID. mapping(uint256 => Content) public contents; + + // Mapping to track rental expiration timestamps for each account and content. mapping(address => mapping(uint256 => uint256)) private rentals; + /// @notice Constructor for the RentalPolicy contract. + /// @param rmAddress Address of the Rights Manager (RM) contract. + /// @param ownershipAddress Address of the Ownership contract. constructor( address rmAddress, address ownershipAddress ) BasePolicy(rmAddress, ownershipAddress) {} + /// @notice Returns the name of the policy. + /// @return The name of the policy, "RentalPolicy". function name() external pure override returns (string memory) { return "RentalPolicy"; } + /// @notice Returns the business/strategy model implemented by the policy. + /// @return A detailed description of the policy's rental model. + function description() external pure override returns (bytes memory) { + return + abi.encodePacked( + "The RentalPolicy implements a content rental strategy where users pay a fixed fee to access digital content " + "for a limited period. The strategy is focused on temporary access, allowing content holders to monetize their assets " + "through short-term rentals without transferring full ownership. Key aspects of this policy include: \n\n" + "1) Flexible rental duration: Each content can have a customized rental period defined by the content holder. \n" + "2) Pay-per-use model: Users pay a one-time fee per rental, providing a cost-effective way to access content without a long-term commitment.\n " + "3) Automated rental management: Once the rental fee is paid, the content becomes accessible to the user for the specified duration,\n " + "after which access is automatically revoked.\n " + "4) Secure revenue distribution: The rental fee is transferred directly to the content holder through the TreasuryHelper, ensuring secure and \n" + "timely payments. This policy provides a straightforward and transparent way for content owners to generate revenue from their digital assets \n" + "while giving users temporary access to premium content." + ); + } + + /// @notice Registers content with rental terms including duration and price. + /// @dev Only callable for registered content IDs. + /// @param contentId The ID of the content to register. + /// @param rentalDuration Duration (in seconds) for which the content will be rented. + /// @param price The price to rent the content. function registerContent( uint256 contentId, uint256 rentalDuration, @@ -33,22 +69,25 @@ contract RentalPolicy is BasePolicy, IPolicy { contents[contentId] = Content(rentalDuration, price); } + /// @dev Internal function to register the rental of content for a specific account. + /// @param account The address of the account renting the content. + /// @param contentId The ID of the content being rented. + /// @param expire The expiration time (in seconds) for the rental. function _registerRent( address account, uint256 contentId, uint256 expire ) private { - // setup renting condition.. rentals[account][contentId] = block.timestamp + expire; } - /// @notice Exec the deal between the content holder and the account based on the policy's rules. - /// @dev This method is expected to be called only by RM contract and its used to establish - /// any logic related to access, validations, etc... - /// @param deal The deal object containing the terms agreed upon between the content holder and the account. - /// @param data Additional data required for processing the deal. - /// @return bool A boolean indicating whether the deal was successfully executed (`true`) or not (`false`). - /// @return string A message providing context for the execution result. + /// @notice Executes the deal between the content holder and the account based on the policy's rules. + /// @dev This function is expected to be called only by the Rights Manager (RM) contract. + /// It handles any logic related to access and validation of the rental terms. + /// @param deal The deal object containing the agreed terms between the content holder and the account. + /// @param data Additional data required for processing the deal, e.g., content ID. + /// @return bool Indicates whether the deal was successfully executed. + /// @return string Provides a message describing the result of the execution. function exec( T.Deal calldata deal, bytes calldata data @@ -56,41 +95,40 @@ contract RentalPolicy is BasePolicy, IPolicy { uint256 contentId = abi.decode(data, (uint256)); Content memory content = contents[contentId]; - if (contentId == 0) return (false, "Invalid content id"); + if (contentId == 0) return (false, "Invalid content ID"); if (getHolder(contentId) != deal.holder) - return (false, "Invalid content id holder"); + return (false, "Invalid content ID holder"); if (deal.total < content.price) return (false, "Insufficient funds for rental"); - // The rigths manager send funds to policy before call this method - // then the logic of distribution could be here... - // example transfering all the revenues to content holder.. + // Transfer the funds to the content holder. deal.holder.transfer(deal.available, deal.currency); + // Register the rental for the account with the rental duration. _registerRent(deal.account, contentId, content.rentalDuration); - return (true, "ok"); + + return (true, "Rental successfully executed"); } /// @notice Retrieves the access terms for a specific account and content ID. /// @param account The address of the account for which access terms are being retrieved. /// @param contentId The ID of the content associated with the access terms. - /// @return The access terms as a `bytes` array, which can contain any necessary data - /// for validating on-chain or off-chain access. eg: PILTerms https://docs.story.foundation/docs/pil-terms + /// @return The access terms as a `bytes` array, which can contain the rental expiration timestamp. function terms( address account, uint256 contentId ) external view override returns (bytes memory) { - // any data needed to validate by distributors can be returned here.. return abi.encode(rentals[account][contentId]); } - /// @notice Verify whether the on-chain access terms for an account and content ID are satisfied. + /// @notice Verifies whether the on-chain access terms for an account and content ID are satisfied. /// @param account The address of the account to check. - /// @param contentId The content ID to check against. + /// @param contentId The ID of the content to check against. + /// @return bool Returns `true` if the rental period is still valid, `false` otherwise. function comply( address account, uint256 contentId ) external view override returns (bool) { - // the condition to validate access to content by the account.. + // Check if the current time is before the rental expiration. return block.timestamp <= rentals[account][contentId]; } } diff --git a/examples/SubscriptionPolicy.sol b/examples/SubscriptionPolicy.sol index 182dd49..36cb507 100644 --- a/examples/SubscriptionPolicy.sol +++ b/examples/SubscriptionPolicy.sol @@ -1,27 +1,56 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; + import "contracts/base/BasePolicy.sol"; import "contracts/interfaces/IPolicy.sol"; import "contracts/libraries/Types.sol"; +/// @title SubscriptionPolicy +/// @notice Implements a subscription-based content access policy, allowing users to subscribe to content catalogs for a set duration. contract SubscriptionPolicy is BasePolicy, IPolicy { + /// @dev Structure to define a subscription package. struct Package { - uint256 subscriptionDuration; - uint256 price; + uint256 subscriptionDuration; // Duration in seconds for which the subscription is valid. + uint256 price; // Price of the subscription package. } + // Mapping from content holder (address) to their subscription package details. mapping(address => Package) public packages; + + // Mapping to track subscription expiration for each user (account) and content holder. mapping(address => mapping(address => uint256)) private subscriptions; + /// @notice Constructor for the SubscriptionPolicy contract. + /// @param rmAddress Address of the Rights Manager (RM) contract. + /// @param ownershipAddress Address of the Ownership contract. constructor( address rmAddress, address ownershipAddress ) BasePolicy(rmAddress, ownershipAddress) {} + /// @notice Returns the name of the policy. + /// @return The name of the policy, "SubscriptionPolicy". function name() external pure override returns (string memory) { return "SubscriptionPolicy"; } + /// @notice Returns the business/strategy model implemented by the policy. + /// @return A detailed description of the subscription policy as bytes. + function description() external pure override returns (bytes memory) { + return + abi.encodePacked( + "This policy implements a subscription-based model where users pay a fixed fee ", + "to access a content holder's catalog for a specified duration.\n\n", + "1) Flexible subscription duration, defined by the content holder.\n", + "2) Recurring revenue streams for content holders.\n", + "3) Immediate access to content catalog during the subscription period.\n", + "4) Automated payment processing." + ); + } + + /// @notice Registers a subscription package for the content holder. + /// @param subscriptionDuration The duration of the subscription in seconds. + /// @param price The price of the subscription package. function registerPackage( uint256 subscriptionDuration, uint256 price @@ -44,10 +73,12 @@ contract SubscriptionPolicy is BasePolicy, IPolicy { return (false, "Insufficient funds for subscription"); // set rental expire + // Transfer the funds to the content holder. + deal.holder.transfer(deal.available, deal.currency); uint256 subTime = block.timestamp + pck.subscriptionDuration; // subscribe to content owner's catalog (content package) subscriptions[deal.account][deal.holder] = subTime; - return (true, "success"); + return (true, "ok"); } function terms( @@ -65,10 +96,4 @@ contract SubscriptionPolicy is BasePolicy, IPolicy { address holder = getHolder(contentId); return block.timestamp <= subscriptions[account][holder]; } - - // Define cómo se manejarán los pagos de suscripción - function shares( - address account, - uint256 contentId - ) external view override returns (T.Shares[] memory) {} }