diff --git a/contracts/Distributor.sol b/contracts/Distributor.sol index 57c0711..8e9fefd 100644 --- a/contracts/Distributor.sol +++ b/contracts/Distributor.sol @@ -51,7 +51,7 @@ contract Distributor is __ERC165_init(); __Ownable_init(_owner); __CurrencyManager_init(); - __Fees_init(1, address(0)); + __Fees_init(0, address(0)); if (bytes(_endpoint).length == 0) revert InvalidEndpoint(); endpoint = _endpoint; diff --git a/contracts/RightsManager.sol b/contracts/RightsManager.sol index b8e85b9..f4efe3b 100644 --- a/contracts/RightsManager.sol +++ b/contracts/RightsManager.sol @@ -23,7 +23,7 @@ import "contracts/base/upgradeable/extensions/RightsManagerDelegationUpgradeable import "contracts/interfaces/IRegistrableVerifiable.sol"; import "contracts/interfaces/IReferendumVerifiable.sol"; import "contracts/interfaces/IRightsManager.sol"; -import "contracts/interfaces/IStrategy.sol"; +import "contracts/interfaces/ILicense.sol"; import "contracts/interfaces/IDistributor.sol"; import "contracts/interfaces/IRepository.sol"; import "contracts/libraries/TreasuryHelper.sol"; @@ -86,6 +86,7 @@ contract RightsManager is error InvalidNotApprovedContent(); error InvalidNotAllowedContent(); error InvalidUnknownContent(); + error InvalidAlreadyRegisteredContent(); error NoFundsToWithdraw(address); error NoDeal(string reason); @@ -177,7 +178,7 @@ contract RightsManager is } /// @notice Allocates the specified amount across a distribution array and returns the remaining unallocated amount. - /// @dev Distributes the amount based on the provided distribution array. + /// @dev Distributes the amount based on the provided distribution array. /// Ensures no more than 100 allocations and a minimum of 1% per distributor. /// @param amount The total amount to be allocated. /// @param currency The address of the currency being allocated. @@ -356,6 +357,11 @@ contract RightsManager is address to, uint256 contentId ) external onlyApprovedContent(to, contentId) { + // if(ownerOf(contentId) == to){ + // // if the owner is trying to re-mint, nothing happens.. + // return; + // } + _mint(to, contentId); emit RegisteredContent(contentId); } @@ -383,25 +389,25 @@ contract RightsManager is } /// @inheritdoc IRightsDelegable - /// @notice Delegates rights for a specific content ID to a grantee. + /// @notice Delegates rights for a specific content ID to a license validator. /// @param validator The address of strategy license validator contract to delegate rights to. /// @param contentId The content ID for which rights are being delegated. function grantRights( address validator, uint256 contentId - ) external onlyHolder(contentId) onlyStrategyContract(validator) { + ) external onlyHolder(contentId) onlyLicenseContract(validator) { _grantRights(validator, contentId); emit RightsDelegated(validator, contentId); } /// @inheritdoc IRightsDelegable - /// @notice Delegates rights for a specific content ID to a grantee. + /// @notice Delegates rights for a specific content ID to a license validator. /// @param validator The address of strategy license validator contract to revoke rights to. /// @param contentId The content ID for which rights are being revoked. function revokeRights( address validator, uint256 contentId - ) external onlyHolder(contentId) onlyStrategyContract(validator) { + ) external onlyHolder(contentId) onlyLicenseContract(validator) { _revokeRights(validator, contentId); emit RightsRevoked(validator, contentId); } @@ -425,16 +431,15 @@ contract RightsManager is if (!isEligibleForDistribution(contentId)) revert InvalidNotAllowedContent(); - // the sender MUST be a IStrategy license advocate/validator + // the sender MUST be a ILicense advocate/validator address advocate = _msgSender(); - IStrategy strategy = IStrategy(advocate); + ILicense license = ILicense(advocate); IDistributor distributor = IDistributor(getCustodial(contentId)); - T.Allocation memory alloc = strategy.allocation(account, contentId); + T.Allocation memory alloc = license.allocation(account, contentId); // transaction details uint256 amount = alloc.t9n.amount; address currency = alloc.t9n.currency; - // The user, owner or delegated validator must ensure that the necessary steps // are taken to handle the transaction value or set the appropriate // approve/allowance for the DRM (Digital Rights Management) contract. @@ -448,11 +453,9 @@ contract RightsManager is if (deductions > total) revert NoDeal("The fees are too high."); uint256 remaining = _allocate(total - deductions, currency, alloc.d10n); - address owner = ownerOf(contentId); - address manager = distributor.getManager(); - // register amounts in ledger.. - _sumLedgerEntry(owner, remaining, currency); - _sumLedgerEntry(manager, acceptedSplit, currency); + // register split distribution in ledger.. + _sumLedgerEntry(ownerOf(contentId), remaining, currency); + _sumLedgerEntry(distributor.getManager(), acceptedSplit, currency); _grantAccess(account, contentId, advocate); emit GrantedAccess(account, contentId); } diff --git a/contracts/base/upgradeable/FeesManagerUpgradeable.sol b/contracts/base/upgradeable/FeesManagerUpgradeable.sol index 06e4ee8..029e87d 100644 --- a/contracts/base/upgradeable/FeesManagerUpgradeable.sol +++ b/contracts/base/upgradeable/FeesManagerUpgradeable.sol @@ -65,8 +65,6 @@ abstract contract FeesManagerUpgradeable is Initializable, IFeesManager { /// @param token The address of the token to check. modifier onlySupportedToken(address token) { FeesStorage storage $ = _getFeesStorage(); - // fees == 0 is default for uint256. - // address(0) is equivalent to native token if fees > 0 if (!$._tokenSupported[token]) revert InvalidUnsupportedToken(token); _; } @@ -74,8 +72,8 @@ abstract contract FeesManagerUpgradeable is Initializable, IFeesManager { /// @notice Modifier to ensure only valid basis points are used. /// @param fees The fee amount to check. modifier onlyBasePointsAllowed(uint256 fees) { - // if fees < 1 = 0.01% || fees basis > 10_000 = 100% - if (fees < 1 || fees > C.BPS_MAX) + // fees basis > 10_000 = 100% + if (fees > C.BPS_MAX) revert InvalidBasisPointRange(); _; } @@ -83,8 +81,8 @@ abstract contract FeesManagerUpgradeable is Initializable, IFeesManager { /// @notice Modifier to ensure only valid nominal fees are used. /// @param fees The fee amount to check. modifier onlyNominalAllowed(uint256 fees) { - // if fees < 1% || fees > 100% - if (fees < 1 || fees > C.SCALE_FACTOR) + // fees > 100% + if (fees > C.SCALE_FACTOR) revert InvalidNominalRange(); _; } diff --git a/contracts/base/upgradeable/extensions/RightsManagerContentAccessUpgradeable.sol b/contracts/base/upgradeable/extensions/RightsManagerContentAccessUpgradeable.sol index 01cbe32..05e2a2e 100644 --- a/contracts/base/upgradeable/extensions/RightsManagerContentAccessUpgradeable.sol +++ b/contracts/base/upgradeable/extensions/RightsManagerContentAccessUpgradeable.sol @@ -6,21 +6,21 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; import "contracts/interfaces/IRightsAccessController.sol"; -import "contracts/interfaces/IStrategy.sol"; +import "contracts/interfaces/ILicense.sol"; import "contracts/libraries/Types.sol"; /// @title Rights Manager Content Access Upgradeable /// @notice This abstract contract manages content access control using a license -/// validator contract that must implement the IStrategy interface. +/// validator contract that must implement the ILicense interface. abstract contract RightsManagerContentAccessUpgradeable is Initializable, IRightsAccessController { using ERC165Checker for address; - /// @dev The interface ID for IStrategy, used to verify that a validator contract implements the correct interface. + /// @dev The interface ID for ILicense, used to verify that a validator contract implements the correct interface. bytes4 private constant INTERFACE_STRATEGY_VALIDATOR = - type(IStrategy).interfaceId; + type(ILicense).interfaceId; /// @custom:storage-location erc7201:rightscontentaccess.upgradeable /// @dev Storage struct for the access control list (ACL) that maps content IDs and accounts to validator contracts. @@ -45,17 +45,17 @@ abstract contract RightsManagerContentAccessUpgradeable is } } - /// @dev Error thrown when the validator contract does not implement the IStrategy interface. + /// @dev Error thrown when the validator contract does not implement the ILicense interface. error InvalidStrategyContract(address strategy); /** - * @dev Modifier to check that a strategy validator contract implements the IStrategy interface. - * @param strategy The address of the strategy validator contract. + * @dev Modifier to check that a license contract implements the ILicense interface. + * @param license The address of the license validator contract. * Reverts if the validator does not implement the required interface. */ - modifier onlyStrategyContract(address strategy) { - if (!strategy.supportsInterface(INTERFACE_STRATEGY_VALIDATOR)) { - revert InvalidStrategyContract(strategy); + modifier onlyLicenseContract(address license) { + if (!license.supportsInterface(INTERFACE_STRATEGY_VALIDATOR)) { + revert InvalidStrategyContract(license); } _; } @@ -65,7 +65,7 @@ abstract contract RightsManagerContentAccessUpgradeable is * @dev The function associates a content ID and account with a validator contract in the ACL storage. * @param account The address of the account to be granted access. * @param contentId The ID of the content for which access is being granted. - * @param validator The address of the validator contract that will be used to validate access. + * @param validator The address of the license validator contract that will be used to validate access. */ function _grantAccess( address account, @@ -92,7 +92,7 @@ abstract contract RightsManagerContentAccessUpgradeable is address strategyValidator = $._acl[contentId][account]; // if the access is not registered, return false. if (strategyValidator == address(0)) return false; - // The approved method is called and executed according to the IStrategy specification. - return IStrategy(strategyValidator).license(account, contentId); + // The approved method is called and executed according to the ILicense specification. + return ILicense(strategyValidator).terms(account, contentId); } } diff --git a/contracts/interfaces/IStrategy.sol b/contracts/interfaces/ILicense.sol similarity index 53% rename from contracts/interfaces/IStrategy.sol rename to contracts/interfaces/ILicense.sol index ea641b7..5055ebe 100644 --- a/contracts/interfaces/IStrategy.sol +++ b/contracts/interfaces/ILicense.sol @@ -2,19 +2,19 @@ pragma solidity ^0.8.24; import "contracts/libraries/Types.sol"; -/// @title IStrategy -/// @notice Interface for managing access to content based on conditions, +/// @title ILicense +/// @notice Interface for managing access to content based on licensing terms, /// transactions, and distribution of royalties or fees. -interface IStrategy { - /// @notice Verify whether the license for an account and content ID is still valid - /// @param account The address of the account to approve. - /// @param contentId The content ID to approve against. - function license( +interface ILicense { + /// @notice Verify whether the 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. + function terms( address account, uint256 contentId ) external view returns (bool); - /// @notice Retrieves the allocation spec to distribute the royalties or fees. + /// @notice Retrieves the allocation specification to distribute the royalties or fees. /// @param account The address of the account initiating the transaction. /// @param contentId The content ID related to the transaction. function allocation( diff --git a/contracts/modules/lens/GatedContentModule.sol b/contracts/modules/lens/GatedContentModule.sol new file mode 100644 index 0000000..4861ce4 --- /dev/null +++ b/contracts/modules/lens/GatedContentModule.sol @@ -0,0 +1,193 @@ +// // SPDX-License-Identifier: MIT +// pragma solidity ^0.8.24; + +// import "@openzeppelin/contracts/access/Ownable.sol"; +// import "@openzeppelin/contracts/utils/types/Time.sol"; +// import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +// import "contracts/modules/lens/interfaces/IPublicationActionModule.sol"; +// import "contracts/modules/lens/base/LensModuleMetadata.sol"; +// import "contracts/modules/lens/base/LensModuleRegistrant.sol"; +// import "contracts/modules/lens/base/HubRestricted.sol"; +// import "contracts/interfaces/ILicense.sol"; +// import "contracts/interfaces/IRightsManager.sol"; +// import "contracts/libraries/Constants.sol"; +// import "contracts/libraries/Types.sol"; + +// /** +// * @title GatedContentModule +// * @dev Contract that manages gated content access based on predefined conditions. +// * It inherits from Ownable, IPublicationActionModule, LensModuleMetadata, +// * LensModuleRegistrant, and HubRestricted. +// */ +// contract GatedContentModule is +// Ownable, +// LensModuleMetadata, +// LensModuleRegistrant, +// HubRestricted, +// IPublicationActionModule, +// ILicense +// { +// using SafeERC20 for IERC20; + +// // Custom errors for specific failure cases +// error InvalidExistingContentPublication(); +// error InvalidCondition(); +// error AccessDenied(); + +// struct GateRegistry { +// uint256 contentId; +// address contractAddress; +// string method; +// bytes[] parameters; +// bool isCustomGate; +// } + +// // Mapping from publication ID to content ID +// mapping(uint256 => uint256) contentRegistry; +// mapping(uint256 => mapping(address => GateRegistry)) gatedConditions; + +// /** +// * @dev Constructor that initializes the GatedContentModule contract. +// * @param hub The address of the hub contract. +// * @param registrant The address of the registrant contract. +// * @param drm The address of the DRM contract. +// */ +// constructor( +// address hub, +// address registrant, +// address drm +// ) +// Ownable(_msgSender()) +// HubRestricted(hub) +// LensModuleRegistrant(registrant) +// {} + +// /** +// * @dev Registers a gating condition for a specific content ID. +// * @param contentId The ID of the content to be gated. +// * @param gate The gating condition. +// */ +// function registerGatingCondition( +// uint256 contentId, +// GateRegistry memory gate +// ) external onlyOwner { +// gatedConditions[contentId][gate.contractAddress] = gate; +// } + +// /** +// * @inheritdoc ILensModuleRegistrant +// * @dev Registers the GatedContentModule as a PUBLICATION_ACTION_MODULE. +// * @return bool Success of the registration. +// */ +// function registerModule() public onlyOwner returns (bool) { +// return _registerModule(Types.ModuleType.PUBLICATION_ACTION_MODULE); +// } + +// /** +// * @dev Sets the metadata URI for the GatedContentModule. +// * @param _metadataURI The new metadata URI. +// */ +// function setModuleMetadataURI( +// string calldata _metadataURI +// ) public onlyOwner { +// _setModuleMetadataURI(_metadataURI); +// } + +// /** +// * @dev Initializes a publication action for gating content access. +// * @param profileId The ID of the profile initiating the action. +// * @param pubId The ID of the publication being gated. +// * @param transactionExecutor The address of the executor of the transaction. +// * @param data Additional data required for the action. +// * @return bytes memory The result of the action. +// */ +// function initializePublicationAction( +// uint256, +// uint256 pubId, +// address transactionExecutor, +// bytes calldata data +// ) external override onlyHub returns (bytes memory) { +// Types.GateParams memory gateParams = abi.decode( +// data, +// (Types.GateParams) +// ); + +// IRightsManager drm = IRightsManager(gateParams.drmAddress); +// if (drm.ownerOf(gateParams.contentId) != address(0)) +// revert InvalidExistingContentPublication(); + +// contentRegistry[pubId] = gateParams.contentId; +// drm.mint(transactionExecutor, gateParams.contentId); +// drm.grantCustodial( +// gateParams.contentId, +// gateParams.distributor, +// gateParams.encryptedContent +// ); + +// drm.grantRights(address(this), gateParams.contentId); +// registerGatingCondition(gateParams.contentId, gateParams.gate); + +// return data; +// } + +// /// @inheritdoc ILicense +// /// @notice Checks whether the terms (such as gated content conditions) for an account and content ID are satisfied. +// /// @param account The address of the account being checked. +// /// @param contentId The content ID associated with the access terms. +// /// @return bool True if the terms are satisfied, false otherwise. +// function terms( +// address account, +// uint256 contentId +// ) external view override returns (bool) { +// GateRegistry memory gate = gatedConditions[contentId][msg.sender]; +// return _checkGate(account, gate); +// } + +// /// @inheritdoc ILicense +// /// @notice Manages the allocation of royalties or fees based on gated content access. +// /// @param account The address of the account initiating the transaction. +// /// @param contentId The content ID related to the transaction. +// /// @return T.Allocation The allocation details for the transaction. +// function allocation( +// address account, +// uint256 contentId +// ) external override returns (T.Allocation memory) { +// GateRegistry memory gate = gatedConditions[contentId][msg.sender]; +// return +// T.Allocation( +// T.Transaction(address(0), 0), +// new T.Distribution +// ); +// } + +// /** +// * @dev Internal function to check if a gating condition is met. +// * @param account The address of the account. +// * @param gate The gating condition. +// * @return bool True if the gate is passed, false otherwise. +// */ +// function _checkGate( +// address account, +// GateRegistry memory gate +// ) internal view returns (bool) { +// (bool success, bytes memory result) = gate.contractAddress.staticcall( +// abi.encodeWithSignature(gate.method, account, gate.parameters) +// ); +// return success && abi.decode(result, (bool)); +// } + +// /** +// * @dev Checks if the contract supports a specific interface. +// * @param interfaceID The ID of the interface to check. +// * @return bool True if the contract supports the interface, false otherwise. +// */ +// function supportsInterface( +// bytes4 interfaceID +// ) public pure override returns (bool) { +// return +// interfaceID == type(IPublicationActionModule).interfaceId || +// interfaceID == type(ILicense).interfaceId || +// super.supportsInterface(interfaceID); +// } +// } diff --git a/contracts/modules/lens/RentModule.sol b/contracts/modules/lens/RentModule.sol index c900451..6ddd32e 100644 --- a/contracts/modules/lens/RentModule.sol +++ b/contracts/modules/lens/RentModule.sol @@ -13,7 +13,7 @@ import "contracts/modules/lens/base/HubRestricted.sol"; import "contracts/modules/lens/libraries/Types.sol"; import "contracts/base/DRMRestricted.sol"; -import "contracts/interfaces/IStrategy.sol"; +import "contracts/interfaces/ILicense.sol"; import "contracts/interfaces/IRightsManager.sol"; import "contracts/libraries/Constants.sol"; import "contracts/libraries/Types.sol"; @@ -31,7 +31,7 @@ contract RentModule is HubRestricted, DRMRestricted, IPublicationActionModule, - IStrategy + ILicense { using SafeERC20 for IERC20; @@ -192,18 +192,21 @@ contract RentModule is Time.timestamp() + (_days * 1 days) ); - IRightsManager(drmAddress).grantAccess(rentalWatcher, contentId); + IRightsManager(drmAddress).registerLicense(rentalWatcher, contentId); return abi.encode(rentRegistry[contentId][rentalWatcher], currency); } - /// @inheritdoc IStrategy - /// @notice Checks whether the license (rental period) for an account and content ID is still valid. - /// @dev This function checks if the current timestamp is within the valid rental period (timelock) for the specified account and content ID. - /// If the current time is within the allowed period, the license is considered valid. + // TODO El mint si no existe hacer el mint, si existe y es el dueƱo que manda, solo omitir, de lo contrario lanzar un error de que ya existe + // mintWithSignature? + + /// @inheritdoc ILicense + /// @notice Checks whether the terms (such as rental period) for an account and content ID are still valid. + /// @dev This function checks if the current timestamp is within the valid period (timelock) for the specified account and content ID. + /// If the current time is within the allowed period, the terms are considered satisfied. /// @param account The address of the account being checked. - /// @param contentId The content ID associated with the license. - /// @return bool True if the license is still valid, false otherwise. - function license( + /// @param contentId The content ID associated with the access terms. + /// @return bool True if the terms are satisfied, false otherwise. + function terms( address account, uint256 contentId ) external view returns (bool) { @@ -212,7 +215,7 @@ contract RentModule is return Time.timestamp() > expireAt; } - /// @inheritdoc IStrategy + /// @inheritdoc ILicense /// @notice Manages the transfer of rental payments and sets up royalty allocation for a given account and content ID. /// @dev This function transfers the specified amount of tokens from the account /// to the contract and increases the allowance for the DRM contract, facilitating royalty payments and access rights. @@ -254,7 +257,7 @@ contract RentModule is ) public pure override returns (bool) { return interfaceID == type(IPublicationActionModule).interfaceId || - interfaceID == type(IStrategy).interfaceId || + interfaceID == type(ILicense).interfaceId || super.supportsInterface(interfaceID); } } diff --git a/test/Syndication.ts b/test/Syndication.ts index 80914e7..c0567ef 100644 --- a/test/Syndication.ts +++ b/test/Syndication.ts @@ -84,7 +84,6 @@ describe('Syndication', function () { const syndication = await deploySyndicationWithFakeGovernor() // min = 1 = 0.01; max = 10_000 = 100% await expect(syndication.setPenaltyRate(10_001)).to.revertedWithCustomError(syndication, 'InvalidBasisPointRange') - await expect(syndication.setPenaltyRate(0)).to.revertedWithCustomError(syndication, 'InvalidBasisPointRange') }) it('Should fail setting penalty rate if not called by governor', async () => {