// SPDX-License-Identifier: MIT pragma solidity ^0.8.13; import "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol"; import "../asset/Assets.sol"; import "../metahub/Protocol.sol"; import "../listing/Listings.sol"; import "../warper/warper-manager/Warpers.sol"; import "../tax/tax-terms-registry/ITaxTermsRegistry.sol"; import "../accounting/token-quote/ITokenQuote.sol"; import "../listing/listing-manager/IListingManager.sol"; import "../metahub/core/IMetahub.sol"; import "../universe/universe-registry/IUniverseRegistry.sol"; import "../listing/listing-configurator/registry/IListingConfiguratorRegistry.sol"; import "../listing/listing-strategy-registry/IListingStrategyRegistry.sol"; import "../listing/listing-strategies/IListingController.sol"; library Rentings { using CountersUpgradeable for CountersUpgradeable.Counter; using EnumerableSetUpgradeable for EnumerableSetUpgradeable.AddressSet; using EnumerableSetUpgradeable for EnumerableSetUpgradeable.UintSet; using Rentings for RenterInfo; using Rentings for Agreement; using Rentings for Registry; using Assets for Assets.AssetId; using Protocol for Protocol.Config; using Listings for Listings.Registry; using Listings for Listings.Listing; using Warpers for Warpers.Registry; using Warpers for Warpers.Warper; /** * @dev Thrown when a rental agreement is being registered for a specific warper ID, * while the previous rental agreement for this warper is still effective. */ error RentalAgreementConflict(uint256 conflictingRentalId); /** * @dev Thrown when attempting to delete effective rental agreement data (before expiration). */ error CannotDeleteEffectiveRentalAgreement(uint256 rentalId); /** * @dev Thrown when attempting to rent for Zero address. */ error RenterCannotBeZeroAddress(); /** * @dev Warper rental status. * NONE - means the warper had never been minted. * AVAILABLE - can be rented. * RENTED - currently rented. */ enum RentalStatus { NONE, AVAILABLE, RENTED } /** * @dev Defines the maximal allowed number of cycles when looking for expired rental agreements. */ uint256 private constant _GC_CYCLES = 20; /** * @dev Rental fee breakdown. */ struct RentalFees { uint256 total; uint256 protocolFee; uint256 listerBaseFee; uint256 listerPremium; uint256 universeBaseFee; uint256 universePremium; IListingTermsRegistry.ListingTerms listingTerms; ITaxTermsRegistry.TaxTerms universeTaxTerms; ITaxTermsRegistry.TaxTerms protocolTaxTerms; } /** * @dev Renting parameters structure. * It is used to encode all the necessary information to estimate and/or fulfill a particular renting request. * @param listingId Listing ID. Also allows to identify the asset(s) being rented. * @param warper Warper address. * @param renter Renter address. * @param rentalPeriod Desired period of asset(s) renting. * @param paymentToken The token address which renter offers as a mean of payment. * @param listingTermsId Listing terms ID. * @param selectedConfiguratorListingTerms */ struct Params { uint256 listingId; address warper; address renter; uint32 rentalPeriod; address paymentToken; uint256 listingTermsId; IListingTermsRegistry.ListingTerms selectedConfiguratorListingTerms; } /** * @dev Rental agreement information. * @param warpedAssets Rented asset(s). * @param universeId The Universe ID. * @param collectionId Warped collection ID. * @param listingId The corresponding ID of the original asset(s) listing. * @param renter The renter account address. * @param startTime The rental agreement staring time. This is the timestamp after which the `renter` * considered to be an warped asset(s) owner. * @param endTime The rental agreement ending time. After this timestamp, the rental agreement is terminated * and the `renter` is no longer the owner of the warped asset(s). * @param listingTerms Listing terms */ struct Agreement { Assets.Asset[] warpedAssets; uint256 universeId; bytes32 collectionId; uint256 listingId; address renter; uint32 startTime; uint32 endTime; AgreementTerms agreementTerms; } struct AgreementTerms { IListingTermsRegistry.ListingTerms listingTerms; ITaxTermsRegistry.TaxTerms universeTaxTerms; ITaxTermsRegistry.TaxTerms protocolTaxTerms; ITokenQuote.PaymentTokenData paymentTokenData; } function isEffective(Agreement storage self) internal view returns (bool) { return self.endTime > uint32(block.timestamp); } function isRegistered(Agreement memory self) internal pure returns (bool) { return self.renter != address(0); } /** * @dev Describes user specific renting information. * @param rentalIndex Renter's set of rental agreement IDs. * @param collectionRentalIndex Mapping from collection ID to the set of rental IDs. */ struct RenterInfo { EnumerableSetUpgradeable.UintSet rentalIndex; mapping(bytes32 => EnumerableSetUpgradeable.UintSet) collectionRentalIndex; } /** * @dev Describes asset(s) specific renting information. * @param latestRentalId Holds the most recent rental agreement ID. */ struct AssetInfo { uint256 latestRentalId; // NOTE: This must never be deleted during cleanup. } /** * @dev Renting registry. * @param idTracker Rental agreement ID tracker (incremental counter). * @param agreements Mapping from rental ID to the rental agreement details. * @param renters Mapping from renter address to the user specific renting info. * @param assets Mapping from asset ID (byte32) to the asset specific renting info. */ struct Registry { CountersUpgradeable.Counter idTracker; mapping(uint256 => Agreement) agreements; mapping(uint256 => Agreement) agreementsHistory; mapping(address => RenterInfo) renters; mapping(bytes32 => AssetInfo) assets; } /** * @dev Returns the number of currently registered rental agreements for particular renter account. */ function userRentalCount(Registry storage self, address renter) internal view returns (uint256) { return self.renters[renter].rentalIndex.length(); } /** * @dev Returns the paginated list of currently registered rental agreements for particular renter account. */ function userRentalAgreements( Registry storage self, address renter, uint256 offset, uint256 limit ) external view returns (uint256[] memory, Rentings.Agreement[] memory) { EnumerableSetUpgradeable.UintSet storage userRentalIndex = self.renters[renter].rentalIndex; uint256 indexSize = userRentalIndex.length(); if (offset >= indexSize) return (new uint256[](0), new Rentings.Agreement[](0)); if (limit > indexSize - offset) { limit = indexSize - offset; } Rentings.Agreement[] memory agreements = new Rentings.Agreement[](limit); uint256[] memory rentalIds = new uint256[](limit); for (uint256 i = 0; i < limit; i++) { rentalIds[i] = userRentalIndex.at(offset + i); agreements[i] = self.agreements[rentalIds[i]]; } return (rentalIds, agreements); } /** * @dev Finds expired user rental agreements associated with `collectionId` and deletes them. * Deletes only first N entries defined by `toBeRemoved` param. * The total number of cycles is capped by GC_CYCLES constant. */ function deleteExpiredUserRentalAgreements( Registry storage self, address renter, bytes32 collectionId, uint256 toBeRemoved ) external { EnumerableSetUpgradeable.UintSet storage rentalIndex = self.renters[renter].collectionRentalIndex[collectionId]; uint256 rentalCount = rentalIndex.length(); if (rentalCount == 0 || toBeRemoved == 0) return; uint256 maxCycles = rentalCount < _GC_CYCLES ? rentalCount : _GC_CYCLES; uint256 removed = 0; for (uint256 i = 0; i < maxCycles; i++) { uint256 rentalId = rentalIndex.at(i); if (!self.agreements[rentalId].isEffective()) { // Warning: we are iterating an array that we are also modifying! _removeRentalAgreement(self, rentalId); removed += 1; maxCycles -= 1; // This is so we account for reduced `rentalCount`. // Stop iterating if we have cleaned up enough desired items. if (removed == toBeRemoved) break; } } } /** * @dev Performs new rental agreement registration. */ function register(Registry storage self, Agreement memory agreement) external returns (uint256 rentalId) { // Generate new rental ID. self.idTracker.increment(); rentalId = self.idTracker.current(); // Save new rental agreement. Agreement storage agreementRecord = self.agreements[rentalId]; agreementRecord.listingId = agreement.listingId; agreementRecord.renter = agreement.renter; agreementRecord.startTime = agreement.startTime; agreementRecord.endTime = agreement.endTime; agreementRecord.collectionId = agreement.collectionId; agreementRecord.agreementTerms.listingTerms = agreement.agreementTerms.listingTerms; agreementRecord.agreementTerms.universeTaxTerms = agreement.agreementTerms.universeTaxTerms; agreementRecord.agreementTerms.protocolTaxTerms = agreement.agreementTerms.protocolTaxTerms; for (uint256 i = 0; i < agreement.warpedAssets.length; i++) { bytes32 assetId = agreement.warpedAssets[i].id.hash(); uint256 latestRentalId = self.assets[assetId].latestRentalId; if (latestRentalId != 0 && self.agreements[latestRentalId].isEffective()) { revert RentalAgreementConflict(latestRentalId); } else { // Add warped assets and their collection ids to rental agreement. agreementRecord.warpedAssets.push(agreement.warpedAssets[i]); // Update warper latest rental ID. self.assets[assetId].latestRentalId = rentalId; } } RenterInfo storage renterInfo = self.renters[agreement.renter]; // Update user rental index. renterInfo.rentalIndex.add(rentalId); // Update user collection rental index. renterInfo.collectionRentalIndex[agreement.collectionId].add(rentalId); } /** * @dev Updates Agreement Record structure in storage and in memory. */ function updateAgreementConfig( Registry storage self, Rentings.Agreement memory inMemoryRentalAgreement, uint256 rentalId, Rentings.RentalFees memory rentalFees, Warpers.Warper memory warper, ITokenQuote.PaymentTokenData memory paymentTokenData ) external returns (Rentings.Agreement memory) { inMemoryRentalAgreement.universeId = warper.universeId; inMemoryRentalAgreement.agreementTerms.listingTerms = rentalFees.listingTerms; inMemoryRentalAgreement.agreementTerms.universeTaxTerms = rentalFees.universeTaxTerms; inMemoryRentalAgreement.agreementTerms.protocolTaxTerms = rentalFees.protocolTaxTerms; inMemoryRentalAgreement.agreementTerms.paymentTokenData = paymentTokenData; Agreement storage agreementRecord = self.agreements[rentalId]; agreementRecord.universeId = inMemoryRentalAgreement.universeId; agreementRecord.agreementTerms.listingTerms = inMemoryRentalAgreement.agreementTerms.listingTerms; agreementRecord.agreementTerms.universeTaxTerms = inMemoryRentalAgreement.agreementTerms.universeTaxTerms; agreementRecord.agreementTerms.protocolTaxTerms = inMemoryRentalAgreement.agreementTerms.protocolTaxTerms; agreementRecord.agreementTerms.paymentTokenData = inMemoryRentalAgreement.agreementTerms.paymentTokenData; return inMemoryRentalAgreement; } /** * @dev Safely removes expired rental data from the registry. */ function removeExpiredRentalAgreement(Registry storage self, uint256 rentalId) external { if (self.agreements[rentalId].isEffective()) revert CannotDeleteEffectiveRentalAgreement(rentalId); _removeRentalAgreement(self, rentalId); } /** * @dev Removes rental data from the registry. */ function _removeRentalAgreement(Registry storage self, uint256 rentalId) private { Agreement storage rentalAgreement = self.agreements[rentalId]; address renter = rentalAgreement.renter; bytes32 collectionId = self.agreements[rentalId].collectionId; self.renters[renter].rentalIndex.remove(rentalId); self.renters[renter].collectionRentalIndex[collectionId].remove(rentalId); self.agreementsHistory[rentalId] = rentalAgreement; // Delete rental agreement. delete self.agreements[rentalId]; } /** * @dev Finds all effective rental agreements from specific collection. * Returns the total value rented by `renter`. */ function collectionRentedValue( Registry storage self, address renter, bytes32 collectionId ) external view returns (uint256 value) { EnumerableSetUpgradeable.UintSet storage rentalIndex = self.renters[renter].collectionRentalIndex[collectionId]; uint256 length = rentalIndex.length(); for (uint256 i = 0; i < length; i++) { Agreement storage agreement = self.agreements[rentalIndex.at(i)]; if (agreement.isEffective()) { for (uint256 j = 0; j < agreement.warpedAssets.length; j++) { value += agreement.warpedAssets[j].value; } } } } /** * @dev Returns asset(s) rental status based on latest rental agreement. */ function assetRentalStatus(Registry storage self, Assets.AssetId memory assetId) external view returns (RentalStatus) { uint256 latestRentalId = self.assets[assetId.hash()].latestRentalId; if (latestRentalId == 0) return RentalStatus.NONE; return self.agreements[latestRentalId].isEffective() ? RentalStatus.RENTED : RentalStatus.AVAILABLE; } /** * @dev Main renting request validation function. */ function validateRentingParams(Params calldata params, IMetahub metahub) external view { // Validate from the renter's perspective. if (params.renter == address(0)) { revert RenterCannotBeZeroAddress(); } // Validate from the listing perspective. IListingManager listingManager = IListingManager(metahub.getContract(Contracts.LISTING_MANAGER)); listingManager.checkRegisteredAndListed(params.listingId); Listings.Listing memory listing = listingManager.listingInfo(params.listingId); listing.checkNotPaused(); listing.checkValidLockPeriod(params.rentalPeriod); // Validate from the warper and strategy override config registry perspective. IWarperManager warperManager = IWarperManager(metahub.getContract(Contracts.WARPER_MANAGER)); warperManager.checkRegisteredWarper(params.warper); Warpers.Warper memory warper = warperManager.warperInfo(params.warper); warper.checkNotPaused(); warper.controller.validateRentingParams(warper, listing.assets, params); // Validate from the universe perspective IUniverseRegistry(metahub.getContract(Contracts.UNIVERSE_REGISTRY)).checkUniversePaymentToken( warper.universeId, params.paymentToken ); IListingTermsRegistry.ListingTerms memory listingTerms; if (listing.configurator != address(0)) { IListingConfiguratorRegistry(metahub.getContract(Contracts.LISTING_CONFIGURATOR_REGISTRY)) .getController(listing.configurator) .validateRenting(params, listing, warper.universeId); listingTerms = params.selectedConfiguratorListingTerms; } else { // Validate from the listing terms perspective IListingTermsRegistry.Params memory listingTermsParams = IListingTermsRegistry.Params({ listingId: params.listingId, universeId: warper.universeId, warperAddress: params.warper }); IListingTermsRegistry listingTermsRegistry = IListingTermsRegistry( metahub.getContract(Contracts.LISTING_TERMS_REGISTRY) ); listingTermsRegistry.checkRegisteredListingTermsWithParams(params.listingTermsId, listingTermsParams); listingTerms = listingTermsRegistry.listingTerms(params.listingTermsId); } bytes4 taxStrategyId = IListingStrategyRegistry(metahub.getContract(Contracts.LISTING_STRATEGY_REGISTRY)) .listingTaxId(listingTerms.strategyId); // Validate from the tax terms perspective ITaxTermsRegistry.Params memory taxTermsParams = ITaxTermsRegistry.Params({ taxStrategyId: taxStrategyId, universeId: warper.universeId, warperAddress: params.warper }); ITaxTermsRegistry taxTermsRegistry = ITaxTermsRegistry(metahub.getContract(Contracts.TAX_TERMS_REGISTRY)); taxTermsRegistry.checkRegisteredUniverseTaxTermsWithParams(taxTermsParams); taxTermsRegistry.checkRegisteredProtocolTaxTermsWithParams(taxTermsParams); } /** * @dev Performs rental fee calculation and returns the fee breakdown. */ function calculateRentalFees( Params calldata rentingParams, Warpers.Warper memory warper, IMetahub metahub ) external view returns (RentalFees memory fees) { // Resolve listing info Listings.Listing memory listing = IListingManager(metahub.getContract(Contracts.LISTING_MANAGER)).listingInfo( rentingParams.listingId ); // Listing terms IListingTermsRegistry.Params memory listingTermsParams; if (listing.configurator != address(0)) { fees.listingTerms = rentingParams.selectedConfiguratorListingTerms; } else { // Compose ListingTerms Params for getting listing terms listingTermsParams = IListingTermsRegistry.Params({ listingId: rentingParams.listingId, universeId: warper.universeId, warperAddress: rentingParams.warper }); // Reading Listing Terms from Listing Terms Registry fees.listingTerms = IListingTermsRegistry(metahub.getContract(Contracts.LISTING_TERMS_REGISTRY)) .listingTerms(rentingParams.listingTermsId); } // Resolve listing controller to calculate lister fee based on selected listing strategy. address listingControllerAddress = IListingStrategyRegistry( metahub.getContract(Contracts.LISTING_STRATEGY_REGISTRY) ).listingController(fees.listingTerms.strategyId); // Resolving all fees using single call to ListingController.calculateRentalFee(...) ( fees.total, fees.listerBaseFee, fees.universeBaseFee, fees.protocolFee, fees.universeTaxTerms, fees.protocolTaxTerms ) = IListingController(listingControllerAddress).calculateRentalFee( listingTermsParams, fees.listingTerms, rentingParams ); // Calculate warper premiums. (uint256 universePremium, uint256 listerPremium) = warper.controller.calculatePremiums( listing.assets, rentingParams, fees.universeBaseFee, fees.listerBaseFee ); // Setting premiums. fees.listerPremium = listerPremium; fees.universePremium = universePremium; // Adding premiums to fees.total. fees.total += fees.listerPremium; fees.total += fees.universePremium; } }