// solhint-disable private-vars-leading-underscore // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableMapUpgradeable.sol"; import "../renting/Rentings.sol"; import "../universe/universe-registry/IUniverseRegistry.sol"; import "../listing/Listings.sol"; import "../contract-registry/Contracts.sol"; import "./IPaymentManager.sol"; import "../listing/listing-strategies/ListingStrategies.sol"; import "../listing/listing-strategies/fixed-rate-with-reward/IFixedRateWithRewardListingController.sol"; import "../tax/tax-strategies/fixed-rate-with-reward/IFixedRateWithRewardTaxController.sol"; library Accounts { using Accounts for Account; using SafeERC20Upgradeable for IERC20Upgradeable; using EnumerableMapUpgradeable for EnumerableMapUpgradeable.AddressToUintMap; /** * @dev Thrown when the estimated rental fee calculated upon renting * is higher than maximal payment amount the renter is willing to pay. */ error RentalFeeSlippage(); /** * @dev Thrown when the amount requested to be paid out is not valid. */ error InvalidWithdrawalAmount(uint256 amount); /** * @dev Thrown when the amount requested to be paid out is larger than available balance. */ error InsufficientBalance(uint256 balance); /** * @dev A structure that describes account balance in ERC20 tokens. */ struct Balance { address token; uint256 amount; } /** * @dev Describes an account state. * @param tokenBalances Mapping from an ERC20 token address to the amount. */ struct Account { EnumerableMapUpgradeable.AddressToUintMap tokenBalances; } /** * @dev Transfers funds from the account balance to the specific address after validating balance sufficiency. */ function withdraw( Account storage self, address token, uint256 amount, address to ) external { if (amount == 0) revert InvalidWithdrawalAmount(amount); uint256 currentBalance = self.balance(token); if (amount > currentBalance) revert InsufficientBalance(currentBalance); unchecked { self.tokenBalances.set(token, currentBalance - amount); } IERC20Upgradeable(token).safeTransfer(to, amount); } struct UserEarning { IPaymentManager.EarningType earningType; bool isLister; address account; uint256 value; address token; } struct UniverseEarning { IPaymentManager.EarningType earningType; uint256 universeId; uint256 value; address token; } struct ProtocolEarning { IPaymentManager.EarningType earningType; uint256 value; address token; } struct RentalEarnings { UserEarning[] userEarnings; UniverseEarning universeEarning; ProtocolEarning protocolEarning; } /** * @dev Redirects handle rental payment from RentingManager to Accounts.Registry * @param self Instance of Accounts.Registry. * @param rentingParams Renting params. * @param fees Rental fees. * @param payer Address of the rent payer. * @param maxPaymentAmount Maximum payment amount. * @return earnings Payment token earnings. */ function handleRentalPayment( Accounts.Registry storage self, Rentings.Params calldata rentingParams, Rentings.RentalFees calldata fees, address payer, uint256 maxPaymentAmount ) external returns (RentalEarnings memory earnings) { IMetahub metahub = IMetahub(address(this)); // Ensure no rental fee payment slippage. if (fees.total > maxPaymentAmount) revert RentalFeeSlippage(); // Handle lister fee component. Listings.Listing memory listing = IListingManager(metahub.getContract(Contracts.LISTING_MANAGER)).listingInfo( rentingParams.listingId ); // Initialize user earnings array. Here we have only one user, who is lister. earnings.userEarnings = new UserEarning[](1); earnings.userEarnings[0] = _createListerEarning( listing, IPaymentManager.EarningType.LISTER_FIXED_FEE, fees.listerBaseFee + fees.listerPremium, rentingParams.paymentToken ); earnings.universeEarning = _createUniverseEarning( IPaymentManager.EarningType.UNIVERSE_FIXED_FEE, IWarperManager(metahub.getContract(Contracts.WARPER_MANAGER)).warperInfo(rentingParams.warper).universeId, fees.universeBaseFee + fees.universePremium, rentingParams.paymentToken ); earnings.protocolEarning = _createProtocolEarning( IPaymentManager.EarningType.PROTOCOL_FIXED_FEE, fees.protocolFee, rentingParams.paymentToken ); performPayouts(self, listing, earnings, payer, rentingParams.paymentToken); } function handleExternalERC20Reward( Accounts.Registry storage self, Listings.Listing memory listing, Rentings.Agreement memory agreement, ERC20RewardDistributionHelper.RentalExternalERC20RewardFees memory rentalExternalERC20RewardFees, address rewardSource ) external returns (RentalEarnings memory earnings) { // Initialize user earnings array. Here we have 2 users: lister and renter. earnings.userEarnings = new UserEarning[](2); earnings.userEarnings[0] = _createListerEarning( listing, IPaymentManager.EarningType.LISTER_EXTERNAL_ERC20_REWARD, rentalExternalERC20RewardFees.listerRewardFee, rentalExternalERC20RewardFees.token ); earnings.userEarnings[1] = _createNonListerEarning( agreement.renter, IPaymentManager.EarningType.RENTER_EXTERNAL_ERC20_REWARD, rentalExternalERC20RewardFees.renterRewardFee, rentalExternalERC20RewardFees.token ); earnings.universeEarning = _createUniverseEarning( IPaymentManager.EarningType.UNIVERSE_EXTERNAL_ERC20_REWARD, agreement.universeId, rentalExternalERC20RewardFees.universeRewardFee, rentalExternalERC20RewardFees.token ); earnings.protocolEarning = _createProtocolEarning( IPaymentManager.EarningType.PROTOCOL_EXTERNAL_ERC20_REWARD, rentalExternalERC20RewardFees.protocolRewardFee, rentalExternalERC20RewardFees.token ); performPayouts(self, listing, earnings, rewardSource, rentalExternalERC20RewardFees.token); } function performPayouts( Accounts.Registry storage self, Listings.Listing memory listing, RentalEarnings memory rentalEarnings, address payer, address payoutToken ) internal { // The amount of payment tokens to be accumulated on the Metahub for future payouts. // This will include all fees which are not being paid out immediately. uint256 accumulatedTokens = 0; // Increase universe balance. self.universes[rentalEarnings.universeEarning.universeId].increaseBalance( rentalEarnings.universeEarning.token, rentalEarnings.universeEarning.value ); accumulatedTokens += rentalEarnings.universeEarning.value; // Increase protocol balance. self.protocol.increaseBalance(rentalEarnings.protocolEarning.token, rentalEarnings.protocolEarning.value); accumulatedTokens += rentalEarnings.protocolEarning.value; UserEarning[] memory userEarnings = rentalEarnings.userEarnings; for (uint256 i = 0; i < userEarnings.length; i++) { UserEarning memory userEarning = userEarnings[i]; if (userEarning.value == 0) continue; if (userEarning.isLister && !listing.immediatePayout) { // If the lister has not requested immediate payout, the earned amount is added to the lister balance. // The direct payout case is handled along with other transfers later. self.users[userEarning.account].increaseBalance(userEarning.token, userEarning.value); accumulatedTokens += userEarning.value; } else { // Proceed with transfers. // If immediate payout requested, transfer the lister earnings directly to the user account. IERC20Upgradeable(userEarning.token).safeTransferFrom(payer, userEarning.account, userEarning.value); } } // Transfer the accumulated token amount from payer to the metahub. if (accumulatedTokens > 0) { IERC20Upgradeable(payoutToken).safeTransferFrom(payer, address(this), accumulatedTokens); } } function _createListerEarning( Listings.Listing memory listing, IPaymentManager.EarningType earningType, uint256 value, address token ) internal pure returns (UserEarning memory listerEarning) { listerEarning = UserEarning({ earningType: earningType, isLister: true, account: listing.beneficiary, value: value, token: token }); } function _createNonListerEarning( address user, IPaymentManager.EarningType earningType, uint256 value, address token ) internal pure returns (UserEarning memory nonListerEarning) { nonListerEarning = UserEarning({ earningType: earningType, isLister: false, account: user, value: value, token: token }); } function _createUniverseEarning( IPaymentManager.EarningType earningType, uint256 universeId, uint256 value, address token ) internal pure returns (UniverseEarning memory universeEarning) { universeEarning = UniverseEarning({ earningType: earningType, universeId: universeId, value: value, token: token }); } function _createProtocolEarning( IPaymentManager.EarningType earningType, uint256 value, address token ) internal pure returns (ProtocolEarning memory protocolEarning) { protocolEarning = ProtocolEarning({earningType: earningType, value: value, token: token}); } /** * @dev Increments value of the particular account balance. */ function increaseBalance( Account storage self, address token, uint256 amount ) internal { uint256 currentBalance = self.balance(token); self.tokenBalances.set(token, currentBalance + amount); } /** * @dev Returns account current balance. * Does not revert if `token` is not in the map. */ function balance(Account storage self, address token) internal view returns (uint256) { (, uint256 value) = self.tokenBalances.tryGet(token); return value; } /** * @dev Returns the list of account balances in various tokens. */ function balances(Account storage self) internal view returns (Balance[] memory) { uint256 length = self.tokenBalances.length(); Balance[] memory allBalances = new Balance[](length); for (uint256 i = 0; i < length; i++) { (address token, uint256 amount) = self.tokenBalances.at(i); allBalances[i] = Balance({token: token, amount: amount}); } return allBalances; } /** * @dev Account registry. * @param protocol The protocol account state. * @param universes Mapping from a universe ID to the universe account state. * @param users Mapping from a user address to the account state. */ struct Registry { Account protocol; mapping(uint256 => Account) universes; mapping(address => Account) users; } }