diff --git a/contracts/JBController3_2.sol b/contracts/JBController3_2.sol new file mode 100644 index 000000000..89f55f1e3 --- /dev/null +++ b/contracts/JBController3_2.sol @@ -0,0 +1,742 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import {ERC165} from '@openzeppelin/contracts/utils/introspection/ERC165.sol'; +import {IERC165} from '@openzeppelin/contracts/utils/introspection/IERC165.sol'; +import {PRBMath} from '@paulrberg/contracts/math/PRBMath.sol'; +import {JBOperatable} from './abstract/JBOperatable.sol'; +import {JBBallotState} from './enums/JBBallotState.sol'; +import {IJBController3_2} from './interfaces/IJBController3_2.sol'; +import {IJBController} from './interfaces/IJBController.sol'; +import {IJBDirectory} from './interfaces/IJBDirectory.sol'; +import {IJBFundAccessConstraintsStore} from './interfaces/IJBFundAccessConstraintsStore.sol'; +import {IJBFundingCycleStore} from './interfaces/IJBFundingCycleStore.sol'; +import {IJBMigratable} from './interfaces/IJBMigratable.sol'; +import {IJBOperatable} from './interfaces/IJBOperatable.sol'; +import {IJBOperatorStore} from './interfaces/IJBOperatorStore.sol'; +import {IJBPaymentTerminal} from './interfaces/IJBPaymentTerminal.sol'; +import {IJBProjects} from './interfaces/IJBProjects.sol'; +import {IJBSplitAllocator} from './interfaces/IJBSplitAllocator.sol'; +import {IJBSplitsStore} from './interfaces/IJBSplitsStore.sol'; +import {IJBTokenStore} from './interfaces/IJBTokenStore.sol'; +import {JBConstants} from './libraries/JBConstants.sol'; +import {JBFundingCycleMetadataResolver3_2} from './libraries/JBFundingCycleMetadataResolver3_2.sol'; +import {JBOperations} from './libraries/JBOperations.sol'; +import {JBSplitsGroups} from './libraries/JBSplitsGroups.sol'; +import {JBFundingCycle} from './structs/JBFundingCycle.sol'; +import {JBSplitAllocationData} from './structs/JBSplitAllocationData.sol'; +import {JBFundAccessConstraints} from './structs/JBFundAccessConstraints.sol'; +import {JBFundingCycle} from './structs/JBFundingCycle.sol'; +import {JBFundingCycleData} from './structs/JBFundingCycleData.sol'; +import {JBFundingCycleMetadata3_2} from './structs/JBFundingCycleMetadata3_2.sol'; +import {JBGroupedSplits} from './structs/JBGroupedSplits.sol'; +import {JBProjectMetadata} from './structs/JBProjectMetadata.sol'; +import {JBSplit} from './structs/JBSplit.sol'; + +/// @notice Stitches together funding cycles and project tokens, making sure all activity is accounted for and correct. +/// @dev This Controller has the same functionality as JBController3_0_1, except it is not backwards compatible with the original IJBController view methods. +contract JBController3_2 is JBOperatable, ERC165, IJBController3_2, IJBMigratable { + // A library that parses the packed funding cycle metadata into a more friendly format. + using JBFundingCycleMetadataResolver3_2 for JBFundingCycle; + + //*********************************************************************// + // --------------------------- custom errors ------------------------- // + //*********************************************************************// + + error BURN_PAUSED_AND_SENDER_NOT_VALID_TERMINAL_DELEGATE(); + error FUNDING_CYCLE_ALREADY_LAUNCHED(); + error INVALID_BASE_CURRENCY(); + error INVALID_REDEMPTION_RATE(); + error INVALID_RESERVED_RATE(); + error MIGRATION_NOT_ALLOWED(); + error MINT_NOT_ALLOWED_AND_NOT_TERMINAL_DELEGATE(); + error NO_BURNABLE_TOKENS(); + error NOT_CURRENT_CONTROLLER(); + error ZERO_TOKENS_TO_MINT(); + + //*********************************************************************// + // --------------------- internal stored properties ------------------ // + //*********************************************************************// + + /// @notice Data regarding the distribution limit of a project during a configuration. + /// @dev bits 0-231: The amount of token that a project can distribute per funding cycle. + /// @dev bits 232-255: The currency of amount that a project can distribute. + /// @custom:param _projectId The ID of the project to get the packed distribution limit data of. + /// @custom:param _configuration The configuration during which the packed distribution limit data applies. + /// @custom:param _terminal The terminal from which distributions are being limited. + /// @custom:param _token The token for which distributions are being limited. + mapping(uint256 => mapping(uint256 => mapping(IJBPaymentTerminal => mapping(address => uint256)))) + internal _packedDistributionLimitDataOf; + + /// @notice Data regarding the overflow allowance of a project during a configuration. + /// @dev bits 0-231: The amount of overflow that a project is allowed to tap into on-demand throughout the configuration. + /// @dev bits 232-255: The currency of the amount of overflow that a project is allowed to tap. + /// @custom:param _projectId The ID of the project to get the packed overflow allowance data of. + /// @custom:param _configuration The configuration during which the packed overflow allowance data applies. + /// @custom:param _terminal The terminal managing the overflow. + /// @custom:param _token The token for which overflow is being allowed. + mapping(uint256 => mapping(uint256 => mapping(IJBPaymentTerminal => mapping(address => uint256)))) + internal _packedOverflowAllowanceDataOf; + + //*********************************************************************// + // --------------- public immutable stored properties ---------------- // + //*********************************************************************// + + /// @notice Mints ERC-721's that represent project ownership. + IJBProjects public immutable override projects; + + /// @notice The contract storing all funding cycle configurations. + IJBFundingCycleStore public immutable override fundingCycleStore; + + /// @notice The contract that manages token minting and burning. + IJBTokenStore public immutable override tokenStore; + + /// @notice The contract that stores splits for each project. + IJBSplitsStore public immutable override splitsStore; + + /// @notice A contract that stores fund access constraints for each project. + IJBFundAccessConstraintsStore public immutable override fundAccessConstraintsStore; + + /// @notice The directory of terminals and controllers for projects. + IJBDirectory public immutable override directory; + + //*********************************************************************// + // --------------------- public stored properties -------------------- // + //*********************************************************************// + + /// @notice The current undistributed reserved token balance of. + mapping(uint256 => uint256) public override reservedTokenBalanceOf; + + //*********************************************************************// + // ------------------------- external views -------------------------- // + //*********************************************************************// + + /// @notice A project's funding cycle for the specified configuration along with its metadata. + /// @param _projectId The ID of the project to which the funding cycle belongs. + /// @return fundingCycle The funding cycle. + /// @return metadata The funding cycle's metadata. + function getFundingCycleOf( + uint256 _projectId, + uint256 _configuration + ) + external + view + override + returns (JBFundingCycle memory fundingCycle, JBFundingCycleMetadata3_2 memory metadata) + { + fundingCycle = fundingCycleStore.get(_projectId, _configuration); + metadata = fundingCycle.expandMetadata(); + } + + /// @notice A project's latest configured funding cycle along with its metadata and the ballot state of the configuration. + /// @param _projectId The ID of the project to which the funding cycle belongs. + /// @return fundingCycle The latest configured funding cycle. + /// @return metadata The latest configured funding cycle's metadata. + /// @return ballotState The state of the configuration. + function latestConfiguredFundingCycleOf( + uint256 _projectId + ) + external + view + override + returns ( + JBFundingCycle memory fundingCycle, + JBFundingCycleMetadata3_2 memory metadata, + JBBallotState ballotState + ) + { + (fundingCycle, ballotState) = fundingCycleStore.latestConfiguredOf(_projectId); + metadata = fundingCycle.expandMetadata(); + } + + /// @notice A project's current funding cycle along with its metadata. + /// @param _projectId The ID of the project to which the funding cycle belongs. + /// @return fundingCycle The current funding cycle. + /// @return metadata The current funding cycle's metadata. + function currentFundingCycleOf( + uint256 _projectId + ) + external + view + override + returns (JBFundingCycle memory fundingCycle, JBFundingCycleMetadata3_2 memory metadata) + { + fundingCycle = fundingCycleStore.currentOf(_projectId); + metadata = fundingCycle.expandMetadata(); + } + + /// @notice A project's queued funding cycle along with its metadata. + /// @param _projectId The ID of the project to which the funding cycle belongs. + /// @return fundingCycle The queued funding cycle. + /// @return metadata The queued funding cycle's metadata. + function queuedFundingCycleOf( + uint256 _projectId + ) + external + view + override + returns (JBFundingCycle memory fundingCycle, JBFundingCycleMetadata3_2 memory metadata) + { + fundingCycle = fundingCycleStore.queuedOf(_projectId); + metadata = fundingCycle.expandMetadata(); + } + + //*********************************************************************// + // -------------------------- public views --------------------------- // + //*********************************************************************// + + /// @notice Gets the current total amount of outstanding tokens for a project. + /// @param _projectId The ID of the project to get total outstanding tokens of. + /// @return The current total amount of outstanding tokens for the project. + function totalOutstandingTokensOf(uint256 _projectId) public view override returns (uint256) { + // Add the reserved tokens to the total supply. + return tokenStore.totalSupplyOf(_projectId) + reservedTokenBalanceOf[_projectId]; + } + + /// @notice Indicates if this contract adheres to the specified interface. + /// @dev See {IERC165-supportsInterface}. + /// @param _interfaceId The ID of the interface to check for adherance to. + /// @return A flag indicating if the provided interface ID is supported. + function supportsInterface( + bytes4 _interfaceId + ) public view virtual override(ERC165, IERC165) returns (bool) { + return + _interfaceId == type(IJBController3_2).interfaceId || + _interfaceId == type(IJBMigratable).interfaceId || + _interfaceId == type(IJBOperatable).interfaceId || + super.supportsInterface(_interfaceId); + } + + //*********************************************************************// + // ---------------------------- constructor -------------------------- // + //*********************************************************************// + + /// @param _operatorStore A contract storing operator assignments. + /// @param _projects A contract which mints ERC-721's that represent project ownership and transfers. + /// @param _directory A contract storing directories of terminals and controllers for each project. + /// @param _fundingCycleStore A contract storing all funding cycle configurations. + /// @param _tokenStore A contract that manages token minting and burning. + /// @param _splitsStore A contract that stores splits for each project. + /// @param _fundAccessConstraintsStore A contract that stores fund access constraints for each project. + constructor( + IJBOperatorStore _operatorStore, + IJBProjects _projects, + IJBDirectory _directory, + IJBFundingCycleStore _fundingCycleStore, + IJBTokenStore _tokenStore, + IJBSplitsStore _splitsStore, + IJBFundAccessConstraintsStore _fundAccessConstraintsStore + ) JBOperatable(_operatorStore) { + projects = _projects; + directory = _directory; + fundingCycleStore = _fundingCycleStore; + tokenStore = _tokenStore; + splitsStore = _splitsStore; + fundAccessConstraintsStore = _fundAccessConstraintsStore; + } + + //*********************************************************************// + // --------------------- external transactions ----------------------- // + //*********************************************************************// + + /// @notice Creates a project. This will mint an ERC-721 into the specified owner's account, configure a first funding cycle, and set up any splits. + /// @dev Each operation within this transaction can be done in sequence separately. + /// @dev Anyone can deploy a project on an owner's behalf. + /// @param _owner The address to set as the owner of the project. The project ERC-721 will be owned by this address. + /// @param _projectMetadata Metadata to associate with the project within a particular domain. This can be updated any time by the owner of the project. + /// @param _data Data that defines the project's first funding cycle. These properties will remain fixed for the duration of the funding cycle. + /// @param _metadata Metadata specifying the controller specific params that a funding cycle can have. These properties will remain fixed for the duration of the funding cycle. + /// @param _mustStartAtOrAfter The time before which the configured funding cycle cannot start. + /// @param _groupedSplits An array of splits to set for any number of groups. + /// @param _fundAccessConstraints An array containing amounts that a project can use from its treasury for each payment terminal. Amounts are fixed point numbers using the same number of decimals as the accompanying terminal. The `_distributionLimit` and `_overflowAllowance` parameters must fit in a `uint232`. + /// @param _terminals Payment terminals to add for the project. + /// @param _memo A memo to pass along to the emitted event. + /// @return projectId The ID of the project. + function launchProjectFor( + address _owner, + JBProjectMetadata calldata _projectMetadata, + JBFundingCycleData calldata _data, + JBFundingCycleMetadata3_2 calldata _metadata, + uint256 _mustStartAtOrAfter, + JBGroupedSplits[] calldata _groupedSplits, + JBFundAccessConstraints[] calldata _fundAccessConstraints, + IJBPaymentTerminal[] memory _terminals, + string memory _memo + ) external virtual override returns (uint256 projectId) { + // Keep a reference to the directory. + IJBDirectory _directory = directory; + + // Mint the project into the wallet of the owner. + projectId = projects.createFor(_owner, _projectMetadata); + + // Set this contract as the project's controller in the directory. + _directory.setControllerOf(projectId, address(this)); + + // Configure the first funding cycle. + uint256 _configuration = _configure( + projectId, + _data, + _metadata, + _mustStartAtOrAfter, + _groupedSplits, + _fundAccessConstraints + ); + + // Add the provided terminals to the list of terminals. + if (_terminals.length > 0) _directory.setTerminalsOf(projectId, _terminals); + + emit LaunchProject(_configuration, projectId, _memo, msg.sender); + } + + /// @notice Creates a funding cycle for an already existing project ERC-721. + /// @dev Each operation within this transaction can be done in sequence separately. + /// @dev Only a project owner or operator can launch its funding cycles. + /// @param _projectId The ID of the project to launch funding cycles for. + /// @param _data Data that defines the project's first funding cycle. These properties will remain fixed for the duration of the funding cycle. + /// @param _metadata Metadata specifying the controller specific params that a funding cycle can have. These properties will remain fixed for the duration of the funding cycle. + /// @param _mustStartAtOrAfter The time before which the configured funding cycle cannot start. + /// @param _groupedSplits An array of splits to set for any number of groups. + /// @param _fundAccessConstraints An array containing amounts that a project can use from its treasury for each payment terminal. Amounts are fixed point numbers using the same number of decimals as the accompanying terminal. The `_distributionLimit` and `_overflowAllowance` parameters must fit in a `uint232`. + /// @param _terminals Payment terminals to add for the project. + /// @param _memo A memo to pass along to the emitted event. + /// @return configuration The configuration of the funding cycle that was successfully created. + function launchFundingCyclesFor( + uint256 _projectId, + JBFundingCycleData calldata _data, + JBFundingCycleMetadata3_2 calldata _metadata, + uint256 _mustStartAtOrAfter, + JBGroupedSplits[] calldata _groupedSplits, + JBFundAccessConstraints[] memory _fundAccessConstraints, + IJBPaymentTerminal[] memory _terminals, + string memory _memo + ) + external + virtual + override + requirePermission(projects.ownerOf(_projectId), _projectId, JBOperations.RECONFIGURE) + returns (uint256 configuration) + { + // If there is a previous configuration, reconfigureFundingCyclesOf should be called instead + if (fundingCycleStore.latestConfigurationOf(_projectId) > 0) + revert FUNDING_CYCLE_ALREADY_LAUNCHED(); + + // Set this contract as the project's controller in the directory. + directory.setControllerOf(_projectId, address(this)); + + // Configure the first funding cycle. + configuration = _configure( + _projectId, + _data, + _metadata, + _mustStartAtOrAfter, + _groupedSplits, + _fundAccessConstraints + ); + + // Add the provided terminals to the list of terminals. + if (_terminals.length > 0) directory.setTerminalsOf(_projectId, _terminals); + + emit LaunchFundingCycles(configuration, _projectId, _memo, msg.sender); + } + + /// @notice Proposes a configuration of a subsequent funding cycle that will take effect once the current one expires if it is approved by the current funding cycle's ballot. + /// @dev Only a project's owner or a designated operator can configure its funding cycles. + /// @param _projectId The ID of the project whose funding cycles are being reconfigured. + /// @param _data Data that defines the funding cycle. These properties will remain fixed for the duration of the funding cycle. + /// @param _metadata Metadata specifying the controller specific params that a funding cycle can have. These properties will remain fixed for the duration of the funding cycle. + /// @param _mustStartAtOrAfter The time before which the configured funding cycle cannot start. + /// @param _groupedSplits An array of splits to set for any number of groups. + /// @param _fundAccessConstraints An array containing amounts that a project can use from its treasury for each payment terminal. Amounts are fixed point numbers using the same number of decimals as the accompanying terminal. The `_distributionLimit` and `_overflowAllowance` parameters must fit in a `uint232`. + /// @param _memo A memo to pass along to the emitted event. + /// @return configuration The configuration of the funding cycle that was successfully reconfigured. + function reconfigureFundingCyclesOf( + uint256 _projectId, + JBFundingCycleData calldata _data, + JBFundingCycleMetadata3_2 calldata _metadata, + uint256 _mustStartAtOrAfter, + JBGroupedSplits[] calldata _groupedSplits, + JBFundAccessConstraints[] calldata _fundAccessConstraints, + string calldata _memo + ) + external + virtual + override + requirePermission(projects.ownerOf(_projectId), _projectId, JBOperations.RECONFIGURE) + returns (uint256 configuration) + { + // Configure the next funding cycle. + configuration = _configure( + _projectId, + _data, + _metadata, + _mustStartAtOrAfter, + _groupedSplits, + _fundAccessConstraints + ); + + emit ReconfigureFundingCycles(configuration, _projectId, _memo, msg.sender); + } + + /// @notice Mint new token supply into an account, and optionally reserve a supply to be distributed according to the project's current funding cycle configuration. + /// @dev Only a project's owner, a designated operator, one of its terminals, or the current data source can mint its tokens. + /// @param _projectId The ID of the project to which the tokens being minted belong. + /// @param _tokenCount The amount of tokens to mint in total, counting however many should be reserved. + /// @param _beneficiary The account that the tokens are being minted for. + /// @param _memo A memo to pass along to the emitted event. + /// @param _preferClaimedTokens A flag indicating whether a project's attached token contract should be minted if they have been issued. + /// @param _useReservedRate Whether to use the current funding cycle's reserved rate in the mint calculation. + /// @return beneficiaryTokenCount The amount of tokens minted for the beneficiary. + function mintTokensOf( + uint256 _projectId, + uint256 _tokenCount, + address _beneficiary, + string calldata _memo, + bool _preferClaimedTokens, + bool _useReservedRate + ) external virtual override returns (uint256 beneficiaryTokenCount) { + // There should be tokens to mint. + if (_tokenCount == 0) revert ZERO_TOKENS_TO_MINT(); + + // Define variables that will be needed outside scoped section below. + // Keep a reference to the reserved rate to use + uint256 _reservedRate; + + // Scoped section prevents stack too deep. `_fundingCycle` only used within scope. + { + // Get a reference to the project's current funding cycle. + JBFundingCycle memory _fundingCycle = fundingCycleStore.currentOf(_projectId); + + // Minting limited to: project owner, authorized callers, project terminal and current funding cycle data source + _requirePermissionAllowingOverride( + projects.ownerOf(_projectId), + _projectId, + JBOperations.MINT, + directory.isTerminalOf(_projectId, IJBPaymentTerminal(msg.sender)) || + msg.sender == address(_fundingCycle.dataSource()) + ); + + // If the message sender is not a terminal or a datasource, the current funding cycle must allow minting. + if ( + !_fundingCycle.mintingAllowed() && + !directory.isTerminalOf(_projectId, IJBPaymentTerminal(msg.sender)) && + msg.sender != address(_fundingCycle.dataSource()) + ) revert MINT_NOT_ALLOWED_AND_NOT_TERMINAL_DELEGATE(); + + // Determine the reserved rate to use. + _reservedRate = _useReservedRate ? _fundingCycle.reservedRate() : 0; + + // Override the claimed token preference with the funding cycle value. + _preferClaimedTokens = _preferClaimedTokens == true + ? _preferClaimedTokens + : _fundingCycle.preferClaimedTokenOverride(); + } + + if (_reservedRate != JBConstants.MAX_RESERVED_RATE) { + // The unreserved token count that will be minted for the beneficiary. + beneficiaryTokenCount = PRBMath.mulDiv( + _tokenCount, + JBConstants.MAX_RESERVED_RATE - _reservedRate, + JBConstants.MAX_RESERVED_RATE + ); + + // Mint the tokens. + tokenStore.mintFor(_beneficiary, _projectId, beneficiaryTokenCount, _preferClaimedTokens); + } + + // Add reserved tokens if needed + if (_reservedRate > 0) + reservedTokenBalanceOf[_projectId] += _tokenCount - beneficiaryTokenCount; + + emit MintTokens( + _beneficiary, + _projectId, + _tokenCount, + beneficiaryTokenCount, + _memo, + _reservedRate, + msg.sender + ); + } + + /// @notice Burns a token holder's supply. + /// @dev Only a token's holder, a designated operator, or a project's terminal can burn it. + /// @param _holder The account that is having its tokens burned. + /// @param _projectId The ID of the project to which the tokens being burned belong. + /// @param _tokenCount The number of tokens to burn. + /// @param _memo A memo to pass along to the emitted event. + /// @param _preferClaimedTokens A flag indicating whether a project's attached token contract should be burned first if they have been issued. + function burnTokensOf( + address _holder, + uint256 _projectId, + uint256 _tokenCount, + string calldata _memo, + bool _preferClaimedTokens + ) + external + virtual + override + requirePermissionAllowingOverride( + _holder, + _projectId, + JBOperations.BURN, + directory.isTerminalOf(_projectId, IJBPaymentTerminal(msg.sender)) + ) + { + // There should be tokens to burn + if (_tokenCount == 0) revert NO_BURNABLE_TOKENS(); + + // Get a reference to the project's current funding cycle. + JBFundingCycle memory _fundingCycle = fundingCycleStore.currentOf(_projectId); + + // If the message sender is a terminal, the current funding cycle must not be paused. + if ( + _fundingCycle.burnPaused() && + !directory.isTerminalOf(_projectId, IJBPaymentTerminal(msg.sender)) + ) revert BURN_PAUSED_AND_SENDER_NOT_VALID_TERMINAL_DELEGATE(); + + // Burn the tokens. + tokenStore.burnFrom(_holder, _projectId, _tokenCount, _preferClaimedTokens); + + emit BurnTokens(_holder, _projectId, _tokenCount, _memo, msg.sender); + } + + /// @notice Distributes all outstanding reserved tokens for a project. + /// @param _projectId The ID of the project to which the reserved tokens belong. + /// @param _memo A memo to pass along to the emitted event. + /// @return The amount of minted reserved tokens. + function distributeReservedTokensOf( + uint256 _projectId, + string calldata _memo + ) external virtual override returns (uint256) { + return _distributeReservedTokensOf(_projectId, _memo); + } + + /// @notice Allows other controllers to signal to this one that a migration is expected for the specified project. + /// @dev This controller should not yet be the project's controller. + /// @param _projectId The ID of the project that will be migrated to this controller. + /// @param _from The controller being migrated from. + function prepForMigrationOf(uint256 _projectId, address _from) external virtual override { + _projectId; // Prevents unused var compiler and natspec complaints. + _from; // Prevents unused var compiler and natspec complaints. + } + + /// @notice Allows a project to migrate from this controller to another. + /// @dev Only a project's owner or a designated operator can migrate it. + /// @param _projectId The ID of the project that will be migrated from this controller. + /// @param _to The controller to which the project is migrating. + function migrate( + uint256 _projectId, + IJBMigratable _to + ) + external + virtual + override + requirePermission(projects.ownerOf(_projectId), _projectId, JBOperations.MIGRATE_CONTROLLER) + { + // Keep a reference to the directory. + IJBDirectory _directory = directory; + + // This controller must be the project's current controller. + if (_directory.controllerOf(_projectId) != address(this)) revert NOT_CURRENT_CONTROLLER(); + + // Get a reference to the project's current funding cycle. + JBFundingCycle memory _fundingCycle = fundingCycleStore.currentOf(_projectId); + + // Migration must be allowed. + if (!_fundingCycle.controllerMigrationAllowed()) revert MIGRATION_NOT_ALLOWED(); + + // All reserved tokens must be minted before migrating. + if (reservedTokenBalanceOf[_projectId] != 0) _distributeReservedTokensOf(_projectId, ''); + + // Make sure the new controller is prepped for the migration. + _to.prepForMigrationOf(_projectId, address(this)); + + // Set the new controller. + _directory.setControllerOf(_projectId, address(_to)); + + emit Migrate(_projectId, _to, msg.sender); + } + + //*********************************************************************// + // ------------------------ internal functions ----------------------- // + //*********************************************************************// + + /// @notice Distributes all outstanding reserved tokens for a project. + /// @param _projectId The ID of the project to which the reserved tokens belong. + /// @param _memo A memo to pass along to the emitted event. + /// @return tokenCount The amount of minted reserved tokens. + function _distributeReservedTokensOf( + uint256 _projectId, + string memory _memo + ) internal returns (uint256 tokenCount) { + // Keep a reference to the token store. + IJBTokenStore _tokenStore = tokenStore; + + // Get the current funding cycle to read the reserved rate from. + JBFundingCycle memory _fundingCycle = fundingCycleStore.currentOf(_projectId); + + // Get a reference to the number of tokens that need to be minted. + tokenCount = reservedTokenBalanceOf[_projectId]; + + // Reset the reserved token balance + reservedTokenBalanceOf[_projectId] = 0; + + // Get a reference to the project owner. + address _owner = projects.ownerOf(_projectId); + + // Distribute tokens to splits and get a reference to the leftover amount to mint after all splits have gotten their share. + uint256 _leftoverTokenCount = tokenCount == 0 + ? 0 + : _distributeToReservedTokenSplitsOf( + _projectId, + _fundingCycle.configuration, + JBSplitsGroups.RESERVED_TOKENS, + tokenCount + ); + + // Mint any leftover tokens to the project owner. + if (_leftoverTokenCount > 0) + _tokenStore.mintFor(_owner, _projectId, _leftoverTokenCount, false); + + emit DistributeReservedTokens( + _fundingCycle.configuration, + _fundingCycle.number, + _projectId, + _owner, + tokenCount, + _leftoverTokenCount, + _memo, + msg.sender + ); + } + + /// @notice Distribute tokens to the splits according to the specified funding cycle configuration. + /// @param _projectId The ID of the project for which reserved token splits are being distributed. + /// @param _domain The domain of the splits to distribute the reserved tokens between. + /// @param _group The group of the splits to distribute the reserved tokens between. + /// @param _amount The total amount of tokens to mint. + /// @return leftoverAmount If the splits percents dont add up to 100%, the leftover amount is returned. + function _distributeToReservedTokenSplitsOf( + uint256 _projectId, + uint256 _domain, + uint256 _group, + uint256 _amount + ) internal returns (uint256 leftoverAmount) { + // Keep a reference to the token store. + IJBTokenStore _tokenStore = tokenStore; + + // Set the leftover amount to the initial amount. + leftoverAmount = _amount; + + // Get a reference to the project's reserved token splits. + JBSplit[] memory _splits = splitsStore.splitsOf(_projectId, _domain, _group); + + //Transfer between all splits. + for (uint256 _i; _i < _splits.length; ) { + // Get a reference to the split being iterated on. + JBSplit memory _split = _splits[_i]; + + // The amount to send towards the split. + uint256 _tokenCount = PRBMath.mulDiv( + _amount, + _split.percent, + JBConstants.SPLITS_TOTAL_PERCENT + ); + + // Mints tokens for the split if needed. + if (_tokenCount > 0) { + _tokenStore.mintFor( + // If an allocator is set in the splits, set it as the beneficiary. + // Otherwise if a projectId is set in the split, set the project's owner as the beneficiary. + // If the split has a beneficiary send to the split's beneficiary. Otherwise send to the msg.sender. + _split.allocator != IJBSplitAllocator(address(0)) + ? address(_split.allocator) + : _split.projectId != 0 + ? projects.ownerOf(_split.projectId) + : _split.beneficiary != address(0) + ? _split.beneficiary + : msg.sender, + _projectId, + _tokenCount, + _split.preferClaimed + ); + + // If there's an allocator set, trigger its `allocate` function. + if (_split.allocator != IJBSplitAllocator(address(0))) + _split.allocator.allocate( + JBSplitAllocationData( + address(_tokenStore.tokenOf(_projectId)), + _tokenCount, + 18, + _projectId, + _group, + _split + ) + ); + + // Subtract from the amount to be sent to the beneficiary. + leftoverAmount = leftoverAmount - _tokenCount; + } + + emit DistributeToReservedTokenSplit( + _projectId, + _domain, + _group, + _split, + _tokenCount, + msg.sender + ); + + unchecked { + ++_i; + } + } + } + + /// @notice Configures a funding cycle and stores information pertinent to the configuration. + /// @param _projectId The ID of the project whose funding cycles are being reconfigured. + /// @param _data Data that defines the funding cycle. These properties will remain fixed for the duration of the funding cycle. + /// @param _metadata Metadata specifying the controller specific params that a funding cycle can have. These properties will remain fixed for the duration of the funding cycle. + /// @param _mustStartAtOrAfter The time before which the configured funding cycle cannot start. + /// @param _groupedSplits An array of splits to set for any number of groups. + /// @param _fundAccessConstraints An array containing amounts that a project can use from its treasury for each payment terminal. Amounts are fixed point numbers using the same number of decimals as the accompanying terminal. + /// @return configuration The configuration of the funding cycle that was successfully reconfigured. + function _configure( + uint256 _projectId, + JBFundingCycleData calldata _data, + JBFundingCycleMetadata3_2 calldata _metadata, + uint256 _mustStartAtOrAfter, + JBGroupedSplits[] memory _groupedSplits, + JBFundAccessConstraints[] memory _fundAccessConstraints + ) internal returns (uint256) { + // Make sure the provided reserved rate is valid. + if (_metadata.reservedRate > JBConstants.MAX_RESERVED_RATE) revert INVALID_RESERVED_RATE(); + + // Make sure the provided redemption rate is valid. + if (_metadata.redemptionRate > JBConstants.MAX_REDEMPTION_RATE) + revert INVALID_REDEMPTION_RATE(); + + // Make sure the provided ballot redemption rate is valid. + if (_metadata.baseCurrency > type(uint24).max) revert INVALID_BASE_CURRENCY(); + + // Configure the funding cycle's properties. + JBFundingCycle memory _fundingCycle = fundingCycleStore.configureFor( + _projectId, + _data, + JBFundingCycleMetadataResolver3_2.packFundingCycleMetadata(_metadata), + _mustStartAtOrAfter + ); + + // Set splits for the group. + splitsStore.set(_projectId, _fundingCycle.configuration, _groupedSplits); + + // Set the funds access constraints. + fundAccessConstraintsStore.setFor( + _projectId, + _fundingCycle.configuration, + _fundAccessConstraints + ); + + return _fundingCycle.configuration; + } +} diff --git a/contracts/JBERC20PaymentTerminal3_2.sol b/contracts/JBERC20PaymentTerminal3_2.sol new file mode 100644 index 000000000..384ef035e --- /dev/null +++ b/contracts/JBERC20PaymentTerminal3_2.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IERC20Metadata} from '@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; +import {JBPayoutRedemptionPaymentTerminal3_2} from './abstract/JBPayoutRedemptionPaymentTerminal3_2.sol'; +import {IJBDirectory} from './interfaces/IJBDirectory.sol'; +import {IJBOperatorStore} from './interfaces/IJBOperatorStore.sol'; +import {IJBProjects} from './interfaces/IJBProjects.sol'; +import {IJBSplitsStore} from './interfaces/IJBSplitsStore.sol'; +import {IJBPrices} from './interfaces/IJBPrices.sol'; + +/// @notice Manages the inflows and outflows of an ERC-20 token. +contract JBERC20PaymentTerminal3_1_2 is JBPayoutRedemptionPaymentTerminal3_2 { + using SafeERC20 for IERC20; + + //*********************************************************************// + // -------------------------- internal views ------------------------- // + //*********************************************************************// + + /// @notice Checks the balance of tokens in this contract. + /// @return The contract's balance, as a fixed point number with the same amount of decimals as this terminal. + function _balance() internal view override returns (uint256) { + return IERC20(token).balanceOf(address(this)); + } + + //*********************************************************************// + // -------------------------- constructor ---------------------------- // + //*********************************************************************// + + /// @param _token The token that this terminal manages. + /// @param _payoutSplitsGroup The group that denotes payout splits from this terminal in the splits store. + /// @param _operatorStore A contract storing operator assignments. + /// @param _projects A contract which mints ERC-721's that represent project ownership and transfers. + /// @param _directory A contract storing directories of terminals and controllers for each project. + /// @param _splitsStore A contract that stores splits for each project. + /// @param _prices A contract that exposes price feeds. + /// @param _store A contract that stores the terminal's data. + /// @param _owner The address that will own this contract. + constructor( + IERC20Metadata _token, + uint256 _payoutSplitsGroup, + IJBOperatorStore _operatorStore, + IJBProjects _projects, + IJBDirectory _directory, + IJBSplitsStore _splitsStore, + IJBPrices _prices, + address _store, + address _owner + ) + JBPayoutRedemptionPaymentTerminal3_2( + address(_token), + _token.decimals(), + uint256(uint24(uint160(address(_token)))), // first 24 bits used for currency. + _payoutSplitsGroup, + _operatorStore, + _projects, + _directory, + _splitsStore, + _prices, + _store, + _owner + ) + // solhint-disable-next-line no-empty-blocks + { + + } + + //*********************************************************************// + // ---------------------- internal transactions ---------------------- // + //*********************************************************************// + + /// @notice Transfers tokens. + /// @param _from The address from which the transfer should originate. + /// @param _to The address to which the transfer should go. + /// @param _amount The amount of the transfer, as a fixed point number with the same number of decimals as this terminal. + function _transferFrom(address _from, address payable _to, uint256 _amount) internal override { + _from == address(this) + ? IERC20(token).safeTransfer(_to, _amount) + : IERC20(token).safeTransferFrom(_from, _to, _amount); + } + + /// @notice Logic to be triggered before transferring tokens from this terminal. + /// @param _to The address to which the transfer is going. + /// @param _amount The amount of the transfer, as a fixed point number with the same number of decimals as this terminal. + function _beforeTransferTo(address _to, uint256 _amount) internal override { + IERC20(token).safeIncreaseAllowance(_to, _amount); + } + + /// @notice Logic to be triggered if a transfer should be undone + /// @param _to The address to which the transfer went. + /// @param _amount The amount of the transfer, as a fixed point number with the same number of decimals as this terminal. + function _cancelTransferTo(address _to, uint256 _amount) internal override { + IERC20(token).safeDecreaseAllowance(_to, _amount); + } +} diff --git a/contracts/JBETHPaymentTerminal3_2.sol b/contracts/JBETHPaymentTerminal3_2.sol new file mode 100644 index 000000000..35aef45f8 --- /dev/null +++ b/contracts/JBETHPaymentTerminal3_2.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import {Address} from '@openzeppelin/contracts/utils/Address.sol'; +import {JBPayoutRedemptionPaymentTerminal3_2} from './abstract/JBPayoutRedemptionPaymentTerminal3_2.sol'; +import {IJBDirectory} from './interfaces/IJBDirectory.sol'; +import {IJBOperatorStore} from './interfaces/IJBOperatorStore.sol'; +import {IJBProjects} from './interfaces/IJBProjects.sol'; +import {IJBSplitsStore} from './interfaces/IJBSplitsStore.sol'; +import {IJBPrices} from './interfaces/IJBPrices.sol'; +import {JBCurrencies} from './libraries/JBCurrencies.sol'; +import {JBSplitsGroups} from './libraries/JBSplitsGroups.sol'; +import {JBTokens} from './libraries/JBTokens.sol'; + +/// @notice Manages all inflows and outflows of ETH funds into the protocol ecosystem. +contract JBETHPaymentTerminal3_1_2 is JBPayoutRedemptionPaymentTerminal3_2 { + //*********************************************************************// + // -------------------------- internal views ------------------------- // + //*********************************************************************// + + /// @notice Checks the balance of tokens in this contract. + /// @return The contract's balance, as a fixed point number with the same amount of decimals as this terminal. + function _balance() internal view override returns (uint256) { + return address(this).balance; + } + + //*********************************************************************// + // -------------------------- constructor ---------------------------- // + //*********************************************************************// + + /// @param _baseWeightCurrency The currency to base token issuance on. + /// @param _operatorStore A contract storing operator assignments. + /// @param _projects A contract which mints ERC-721's that represent project ownership and transfers. + /// @param _directory A contract storing directories of terminals and controllers for each project. + /// @param _splitsStore A contract that stores splits for each project. + /// @param _prices A contract that exposes price feeds. + /// @param _store A contract that stores the terminal's data. + /// @param _owner The address that will own this contract. + constructor( + uint256 _baseWeightCurrency, + IJBOperatorStore _operatorStore, + IJBProjects _projects, + IJBDirectory _directory, + IJBSplitsStore _splitsStore, + IJBPrices _prices, + address _store, + address _owner + ) + JBPayoutRedemptionPaymentTerminal3_1_2( + JBTokens.ETH, + 18, // 18 decimals. + JBCurrencies.ETH, + _baseWeightCurrency, + JBSplitsGroups.ETH_PAYOUT, + _operatorStore, + _projects, + _directory, + _splitsStore, + _prices, + _store, + _owner + ) + // solhint-disable-next-line no-empty-blocks + { + + } + + //*********************************************************************// + // ---------------------- internal transactions ---------------------- // + //*********************************************************************// + + /// @notice Transfers tokens. + /// @param _from The address from which the transfer should originate. + /// @param _to The address to which the transfer should go. + /// @param _amount The amount of the transfer, as a fixed point number with the same number of decimals as this terminal. + function _transferFrom(address _from, address payable _to, uint256 _amount) internal override { + _from; // Prevents unused var compiler and natspec complaints. + + Address.sendValue(_to, _amount); + } +} diff --git a/contracts/JBSingleTokenPaymentTerminalStore3_2.sol b/contracts/JBSingleTokenPaymentTerminalStore3_2.sol new file mode 100644 index 000000000..e60a6cc86 --- /dev/null +++ b/contracts/JBSingleTokenPaymentTerminalStore3_2.sol @@ -0,0 +1,806 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import {ReentrancyGuard} from '@openzeppelin/contracts/security/ReentrancyGuard.sol'; +import {PRBMath} from '@paulrberg/contracts/math/PRBMath.sol'; +import {JBBallotState} from './enums/JBBallotState.sol'; +import {IJBController3_1} from './interfaces/IJBController3_1.sol'; +import {IJBDirectory} from './interfaces/IJBDirectory.sol'; +import {IJBFundingCycleDataSource3_1_1} from './interfaces/IJBFundingCycleDataSource3_1_1.sol'; +import {IJBFundingCycleStore} from './interfaces/IJBFundingCycleStore.sol'; +import {IJBPaymentTerminal} from './interfaces/IJBPaymentTerminal.sol'; +import {IJBPrices} from './interfaces/IJBPrices.sol'; +import {IJBSingleTokenPaymentTerminal} from './interfaces/IJBSingleTokenPaymentTerminal.sol'; +import {IJBSingleTokenPaymentTerminalStore3_2} from './interfaces/IJBSingleTokenPaymentTerminalStore3_2.sol'; +import {JBConstants} from './libraries/JBConstants.sol'; +import {JBCurrencies} from './libraries/JBCurrencies.sol'; +import {JBFixedPointNumber} from './libraries/JBFixedPointNumber.sol'; +import {JBFundingCycleMetadataResolver3_2} from './libraries/JBFundingCycleMetadataResolver3_2.sol'; +import {JBFundingCycle} from './structs/JBFundingCycle.sol'; +import {JBPayDelegateAllocation3_1_1} from './structs/JBPayDelegateAllocation3_1_1.sol'; +import {JBPayParamsData} from './structs/JBPayParamsData.sol'; +import {JBRedeemParamsData} from './structs/JBRedeemParamsData.sol'; +import {JBRedemptionDelegateAllocation3_1_1} from './structs/JBRedemptionDelegateAllocation3_1_1.sol'; +import {JBTokenAmount} from './structs/JBTokenAmount.sol'; + +/// @notice Manages all bookkeeping for inflows and outflows of funds from any ISingleTokenPaymentTerminal. +/// @dev This Store expects a project's controller to be an IJBController3_1. +contract JBSingleTokenPaymentTerminalStore3_1_1 is + ReentrancyGuard, + IJBSingleTokenPaymentTerminalStore3_2 +{ + // A library that parses the packed funding cycle metadata into a friendlier format. + using JBFundingCycleMetadataResolver3_2 for JBFundingCycle; + + //*********************************************************************// + // --------------------------- custom errors ------------------------- // + //*********************************************************************// + error INVALID_AMOUNT_TO_SEND_DELEGATE(); + error CURRENCY_MISMATCH(); + error DISTRIBUTION_AMOUNT_LIMIT_REACHED(); + error FUNDING_CYCLE_PAYMENT_PAUSED(); + error FUNDING_CYCLE_DISTRIBUTION_PAUSED(); + error FUNDING_CYCLE_REDEEM_PAUSED(); + error INADEQUATE_CONTROLLER_ALLOWANCE(); + error INADEQUATE_PAYMENT_TERMINAL_STORE_BALANCE(); + error INSUFFICIENT_TOKENS(); + error INVALID_FUNDING_CYCLE(); + error PAYMENT_TERMINAL_MIGRATION_NOT_ALLOWED(); + + //*********************************************************************// + // -------------------------- private constants ---------------------- // + //*********************************************************************// + + /// @notice Ensures a maximum number of decimal points of persisted fidelity on mulDiv operations of fixed point numbers. + uint256 private constant _MAX_FIXED_POINT_FIDELITY = 18; + + //*********************************************************************// + // ---------------- public immutable stored properties --------------- // + //*********************************************************************// + + /// @notice The directory of terminals and controllers for projects. + IJBDirectory public immutable override directory; + + /// @notice The contract storing all funding cycle configurations. + IJBFundingCycleStore public immutable override fundingCycleStore; + + /// @notice The contract that exposes price feeds. + IJBPrices public immutable override prices; + + //*********************************************************************// + // --------------------- public stored properties -------------------- // + //*********************************************************************// + + /// @notice The amount of tokens that each project has for each terminal, in terms of the terminal's token. + /// @dev The balance is represented as a fixed point number with the same amount of decimals as its relative terminal. + /// @custom:param _terminal The terminal to which the balance applies. + /// @custom:param _projectId The ID of the project to get the balance of. + mapping(IJBSingleTokenPaymentTerminal => mapping(uint256 => uint256)) public override balanceOf; + + /// @notice The amount of funds that a project has distributed from its limit during the current funding cycle for each terminal, in terms of the distribution limit's currency. + /// @dev Increases as projects use their preconfigured distribution limits. + /// @dev The used distribution limit is represented as a fixed point number with the same amount of decimals as its relative terminal. + /// @custom:param _terminal The terminal to which the used distribution limit applies. + /// @custom:param _projectId The ID of the project to get the used distribution limit of. + /// @custom:param _fundingCycleNumber The number of the funding cycle during which the distribution limit was used. + mapping(IJBSingleTokenPaymentTerminal => mapping(uint256 => mapping(uint256 => uint256))) + public + override usedDistributionLimitOf; + + /// @notice The amount of funds that a project has used from its allowance during the current funding cycle configuration for each terminal, in terms of the overflow allowance's currency. + /// @dev Increases as projects use their allowance. + /// @dev The used allowance is represented as a fixed point number with the same amount of decimals as its relative terminal. + /// @custom:param _terminal The terminal to which the overflow allowance applies. + /// @custom:param _projectId The ID of the project to get the used overflow allowance of. + /// @custom:param _configuration The configuration of the during which the allowance was used. + mapping(IJBSingleTokenPaymentTerminal => mapping(uint256 => mapping(uint256 => uint256))) + public + override usedOverflowAllowanceOf; + + //*********************************************************************// + // ------------------------- external views -------------------------- // + //*********************************************************************// + + /// @notice Gets the current overflowed amount in a terminal for a specified project. + /// @dev The current overflow is represented as a fixed point number with the same amount of decimals as the specified terminal. + /// @param _terminal The terminal for which the overflow is being calculated. + /// @param _projectId The ID of the project to get overflow for. + /// @return The current amount of overflow that project has in the specified terminal. + function currentOverflowOf( + IJBSingleTokenPaymentTerminal _terminal, + uint256 _projectId + ) external view override returns (uint256) { + // Return the overflow during the project's current funding cycle. + return + _overflowDuring( + _terminal, + _projectId, + fundingCycleStore.currentOf(_projectId), + _terminal.currency() + ); + } + + /// @notice Gets the current overflowed amount for a specified project across all terminals. + /// @param _projectId The ID of the project to get total overflow for. + /// @param _decimals The number of decimals that the fixed point overflow should include. + /// @param _currency The currency that the total overflow should be in terms of. + /// @return The current total amount of overflow that project has across all terminals. + function currentTotalOverflowOf( + uint256 _projectId, + uint256 _decimals, + uint256 _currency + ) external view override returns (uint256) { + return _currentTotalOverflowOf(_projectId, _decimals, _currency); + } + + /// @notice The current amount of overflowed tokens from a terminal that can be reclaimed by the specified number of tokens, using the total token supply and overflow in the ecosystem. + /// @dev If the project has an active funding cycle reconfiguration ballot, the project's ballot redemption rate is used. + /// @dev The current reclaimable overflow is returned in terms of the specified terminal's currency. + /// @dev The reclaimable overflow is represented as a fixed point number with the same amount of decimals as the specified terminal. + /// @param _terminal The terminal from which the reclaimable amount would come. + /// @param _projectId The ID of the project to get the reclaimable overflow amount for. + /// @param _tokenCount The number of tokens to make the calculation with, as a fixed point number with 18 decimals. + /// @param _useTotalOverflow A flag indicating whether the overflow used in the calculation should be summed from all of the project's terminals. If false, overflow should be limited to the amount in the specified `_terminal`. + /// @return The amount of overflowed tokens that can be reclaimed, as a fixed point number with the same number of decimals as the provided `_terminal`. + function currentReclaimableOverflowOf( + IJBSingleTokenPaymentTerminal _terminal, + uint256 _projectId, + uint256 _tokenCount, + bool _useTotalOverflow + ) external view override returns (uint256) { + // Get a reference to the project's current funding cycle. + JBFundingCycle memory _fundingCycle = fundingCycleStore.currentOf(_projectId); + + // Get the amount of current overflow. + // Use the project's total overflow across all of its terminals if the flag species specifies so. Otherwise, use the overflow local to the specified terminal. + uint256 _currentOverflow = _useTotalOverflow + ? _currentTotalOverflowOf(_projectId, _terminal.decimals(), _terminal.currency()) + : _overflowDuring(_terminal, _projectId, _fundingCycle, _terminal.currency()); + + // If there's no overflow, there's no reclaimable overflow. + if (_currentOverflow == 0) return 0; + + // Get the number of outstanding tokens the project has. + uint256 _totalSupply = IJBController3_1(directory.controllerOf(_projectId)) + .totalOutstandingTokensOf(_projectId); + + // Can't redeem more tokens that is in the supply. + if (_tokenCount > _totalSupply) return 0; + + // Return the reclaimable overflow amount. + return + _reclaimableOverflowDuring( + _projectId, + _fundingCycle, + _tokenCount, + _totalSupply, + _currentOverflow + ); + } + + /// @notice The current amount of overflowed tokens from a terminal that can be reclaimed by the specified number of tokens, using the specified total token supply and overflow amounts. + /// @dev If the project has an active funding cycle reconfiguration ballot, the project's ballot redemption rate is used. + /// @param _projectId The ID of the project to get the reclaimable overflow amount for. + /// @param _tokenCount The number of tokens to make the calculation with, as a fixed point number with 18 decimals. + /// @param _totalSupply The total number of tokens to make the calculation with, as a fixed point number with 18 decimals. + /// @param _overflow The amount of overflow to make the calculation with, as a fixed point number. + /// @return The amount of overflowed tokens that can be reclaimed, as a fixed point number with the same number of decimals as the provided `_overflow`. + function currentReclaimableOverflowOf( + uint256 _projectId, + uint256 _tokenCount, + uint256 _totalSupply, + uint256 _overflow + ) external view override returns (uint256) { + // If there's no overflow, there's no reclaimable overflow. + if (_overflow == 0) return 0; + + // Can't redeem more tokens that is in the supply. + if (_tokenCount > _totalSupply) return 0; + + // Get a reference to the project's current funding cycle. + JBFundingCycle memory _fundingCycle = fundingCycleStore.currentOf(_projectId); + + // Return the reclaimable overflow amount. + return + _reclaimableOverflowDuring(_projectId, _fundingCycle, _tokenCount, _totalSupply, _overflow); + } + + //*********************************************************************// + // -------------------------- constructor ---------------------------- // + //*********************************************************************// + + /// @param _directory A contract storing directories of terminals and controllers for each project. + /// @param _fundingCycleStore A contract storing all funding cycle configurations. + /// @param _prices A contract that exposes price feeds. + constructor(IJBDirectory _directory, IJBFundingCycleStore _fundingCycleStore, IJBPrices _prices) { + directory = _directory; + fundingCycleStore = _fundingCycleStore; + prices = _prices; + } + + //*********************************************************************// + // ---------------------- external transactions ---------------------- // + //*********************************************************************// + + /// @notice Records newly contributed tokens to a project. + /// @dev Mints the project's tokens according to values provided by a configured data source. If no data source is configured, mints tokens proportional to the amount of the contribution. + /// @dev The msg.sender must be an IJBSingleTokenPaymentTerminal. The amount specified in the params is in terms of the msg.sender's tokens. + /// @param _payer The original address that sent the payment to the terminal. + /// @param _amount The amount of tokens being paid. Includes the token being paid, the value, the number of decimals included, and the currency of the amount. + /// @param _projectId The ID of the project being paid. + /// @param _beneficiary The specified address that should be the beneficiary of anything that results from the payment. + /// @param _memo A memo to pass along to the emitted event, and passed along to the funding cycle's data source. + /// @param _metadata Bytes to send along to the data source, if one is provided. + /// @return fundingCycle The project's funding cycle during which payment was made. + /// @return tokenCount The number of project tokens that were minted, as a fixed point number with 18 decimals. + /// @return delegateAllocations The amount to send to delegates instead of adding to the local balance. + /// @return memo A memo that should be passed along to the emitted event. + function recordPaymentFrom( + address _payer, + JBTokenAmount calldata _amount, + uint256 _projectId, + address _beneficiary, + string calldata _memo, + bytes memory _metadata + ) + external + override + nonReentrant + returns ( + JBFundingCycle memory fundingCycle, + uint256 tokenCount, + JBPayDelegateAllocation3_1_1[] memory delegateAllocations, + string memory memo + ) + { + // Get a reference to the current funding cycle for the project. + fundingCycle = fundingCycleStore.currentOf(_projectId); + + // The project must have a funding cycle configured. + if (fundingCycle.number == 0) revert INVALID_FUNDING_CYCLE(); + + // Must not be paused. + if (fundingCycle.payPaused()) revert FUNDING_CYCLE_PAYMENT_PAUSED(); + + // The weight according to which new token supply is to be minted, as a fixed point number with 18 decimals. + uint256 _weight; + + // If the funding cycle has configured a data source, use it to derive a weight and memo. + if (fundingCycle.useDataSourceForPay() && fundingCycle.dataSource() != address(0)) { + // Create the params that'll be sent to the data source. + JBPayParamsData memory _data = JBPayParamsData( + IJBSingleTokenPaymentTerminal(msg.sender), + _payer, + _amount, + _projectId, + fundingCycle.configuration, + _beneficiary, + fundingCycle.weight, + fundingCycle.reservedRate(), + _memo, + _metadata + ); + (_weight, memo, delegateAllocations) = IJBFundingCycleDataSource3_1_1( + fundingCycle.dataSource() + ).payParams(_data); + } + // Otherwise use the funding cycle's weight + else { + _weight = fundingCycle.weight; + memo = _memo; + } + + // Scoped section prevents stack too deep. `_balanceDiff` only used within scope. + { + // Keep a reference to the amount that should be added to the project's balance. + uint256 _balanceDiff = _amount.value; + + // Validate all delegated amounts. This needs to be done before returning the delegate allocations to ensure valid delegated amounts. + if (delegateAllocations.length != 0) { + for (uint256 _i; _i < delegateAllocations.length; ) { + // Get a reference to the amount to be delegated. + uint256 _delegatedAmount = delegateAllocations[_i].amount; + + // Validate if non-zero. + if (_delegatedAmount != 0) { + // Can't delegate more than was paid. + if (_delegatedAmount > _balanceDiff) revert INVALID_AMOUNT_TO_SEND_DELEGATE(); + + // Decrement the total amount being added to the balance. + _balanceDiff = _balanceDiff - _delegatedAmount; + } + + unchecked { + ++_i; + } + } + } + + // If there's no amount being recorded, there's nothing left to do. + if (_amount.value == 0) return (fundingCycle, 0, delegateAllocations, memo); + + // Add the correct balance difference to the token balance of the project. + if (_balanceDiff != 0) + balanceOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId] = + balanceOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId] + + _balanceDiff; + } + + // If there's no weight, token count must be 0 so there's nothing left to do. + if (_weight == 0) return (fundingCycle, 0, delegateAllocations, memo); + + // Get a reference to the number of decimals in the amount. (prevents stack too deep). + uint256 _decimals = _amount.decimals; + + // If the terminal should base its weight on a different currency from the terminal's currency, determine the factor. + // The weight is always a fixed point mumber with 18 decimals. To ensure this, the ratio should use the same number of decimals as the `_amount`. + uint256 _weightRatio = _amount.currency == fundingCycle.baseCurrency() + ? 10 ** _decimals + : prices.priceFor(_amount.currency, fundingCycle.baseCurrency(), _decimals); + + // Find the number of tokens to mint, as a fixed point number with as many decimals as `weight` has. + tokenCount = PRBMath.mulDiv(_amount.value, _weight, _weightRatio); + } + + /// @notice Records newly redeemed tokens of a project. + /// @dev Redeems the project's tokens according to values provided by a configured data source. If no data source is configured, redeems tokens along a redemption bonding curve that is a function of the number of tokens being burned. + /// @dev The msg.sender must be an IJBSingleTokenPaymentTerminal. The amount specified in the params is in terms of the msg.senders tokens. + /// @param _holder The account that is having its tokens redeemed. + /// @param _projectId The ID of the project to which the tokens being redeemed belong. + /// @param _tokenCount The number of project tokens to redeem, as a fixed point number with 18 decimals. + /// @param _memo A memo to pass along to the emitted event. + /// @param _metadata Bytes to send along to the data source, if one is provided. + /// @return fundingCycle The funding cycle during which the redemption was made. + /// @return reclaimAmount The amount of terminal tokens reclaimed, as a fixed point number with 18 decimals. + /// @return delegateAllocations The amount to send to delegates instead of sending to the beneficiary. + /// @return memo A memo that should be passed along to the emitted event. + function recordRedemptionFor( + address _holder, + uint256 _projectId, + uint256 _tokenCount, + string memory _memo, + bytes memory _metadata + ) + external + override + nonReentrant + returns ( + JBFundingCycle memory fundingCycle, + uint256 reclaimAmount, + JBRedemptionDelegateAllocation3_1_1[] memory delegateAllocations, + string memory memo + ) + { + // Get a reference to the project's current funding cycle. + fundingCycle = fundingCycleStore.currentOf(_projectId); + + // The current funding cycle must not be paused. + if (fundingCycle.redeemPaused()) revert FUNDING_CYCLE_REDEEM_PAUSED(); + + // Scoped section prevents stack too deep. `_reclaimedTokenAmount`, `_currentOverflow`, and `_totalSupply` only used within scope. + { + // Get a reference to the reclaimed token amount struct, the current overflow, and the total token supply. + JBTokenAmount memory _reclaimedTokenAmount; + uint256 _currentOverflow; + uint256 _totalSupply; + + // Another scoped section prevents stack too deep. `_token`, `_decimals`, and `_currency` only used within scope. + { + // Get a reference to the terminal's tokens. + address _token = IJBSingleTokenPaymentTerminal(msg.sender).token(); + + // Get a reference to the terminal's decimals. + uint256 _decimals = IJBSingleTokenPaymentTerminal(msg.sender).decimals(); + + // Get areference to the terminal's currency. + uint256 _currency = IJBSingleTokenPaymentTerminal(msg.sender).currency(); + + // Get the amount of current overflow. + // Use the local overflow if the funding cycle specifies that it should be used. Otherwise, use the project's total overflow across all of its terminals. + _currentOverflow = fundingCycle.useTotalOverflowForRedemptions() + ? _currentTotalOverflowOf(_projectId, _decimals, _currency) + : _overflowDuring( + IJBSingleTokenPaymentTerminal(msg.sender), + _projectId, + fundingCycle, + _currency + ); + + // Get the number of outstanding tokens the project has. + _totalSupply = IJBController3_1(directory.controllerOf(_projectId)) + .totalOutstandingTokensOf(_projectId); + + // Can't redeem more tokens that is in the supply. + if (_tokenCount > _totalSupply) revert INSUFFICIENT_TOKENS(); + + if (_currentOverflow != 0) + // Calculate reclaim amount using the current overflow amount. + reclaimAmount = _reclaimableOverflowDuring( + _projectId, + fundingCycle, + _tokenCount, + _totalSupply, + _currentOverflow + ); + + _reclaimedTokenAmount = JBTokenAmount(_token, reclaimAmount, _decimals, _currency); + } + + // If the funding cycle has configured a data source, use it to derive a claim amount and memo. + if (fundingCycle.useDataSourceForRedeem() && fundingCycle.dataSource() != address(0)) { + // Yet another scoped section prevents stack too deep. `_state` only used within scope. + { + // Get a reference to the ballot state. + JBBallotState _state = fundingCycleStore.currentBallotStateOf(_projectId); + + // Create the params that'll be sent to the data source. + JBRedeemParamsData memory _data = JBRedeemParamsData( + IJBSingleTokenPaymentTerminal(msg.sender), + _holder, + _projectId, + fundingCycle.configuration, + _tokenCount, + _totalSupply, + _currentOverflow, + _reclaimedTokenAmount, + fundingCycle.useTotalOverflowForRedemptions(), + fundingCycle.redemptionRate(), + _memo, + _metadata + ); + (reclaimAmount, memo, delegateAllocations) = IJBFundingCycleDataSource3_1_1( + fundingCycle.dataSource() + ).redeemParams(_data); + } + } else { + memo = _memo; + } + } + + // Keep a reference to the amount that should be subtracted from the project's balance. + uint256 _balanceDiff = reclaimAmount; + + if (delegateAllocations.length != 0) { + // Validate all delegated amounts. + for (uint256 _i; _i < delegateAllocations.length; ) { + // Get a reference to the amount to be delegated. + uint256 _delegatedAmount = delegateAllocations[_i].amount; + + // Validate if non-zero. + if (_delegatedAmount != 0) + // Increment the total amount being subtracted from the balance. + _balanceDiff = _balanceDiff + _delegatedAmount; + + unchecked { + ++_i; + } + } + } + + // The amount being reclaimed must be within the project's balance. + if (_balanceDiff > balanceOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId]) + revert INADEQUATE_PAYMENT_TERMINAL_STORE_BALANCE(); + + // Remove the reclaimed funds from the project's balance. + if (_balanceDiff != 0) { + unchecked { + balanceOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId] = + balanceOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId] - + _balanceDiff; + } + } + } + + /// @notice Records newly distributed funds for a project. + /// @dev The msg.sender must be an IJBSingleTokenPaymentTerminal. + /// @param _projectId The ID of the project that is having funds distributed. + /// @param _amount The amount to use from the distribution limit, as a fixed point number. + /// @param _currency The currency of the `_amount`. This must match the project's current funding cycle's currency. + /// @return fundingCycle The funding cycle during which the distribution was made. + /// @return distributedAmount The amount of terminal tokens distributed, as a fixed point number with the same amount of decimals as its relative terminal. + function recordDistributionFor( + uint256 _projectId, + uint256 _amount, + uint256 _currency + ) + external + override + nonReentrant + returns (JBFundingCycle memory fundingCycle, uint256 distributedAmount) + { + // Get a reference to the project's current funding cycle. + fundingCycle = fundingCycleStore.currentOf(_projectId); + + // The funding cycle must not be configured to have distributions paused. + if (fundingCycle.distributionsPaused()) revert FUNDING_CYCLE_DISTRIBUTION_PAUSED(); + + // The new total amount that has been distributed during this funding cycle. + uint256 _newUsedDistributionLimitOf = usedDistributionLimitOf[ + IJBSingleTokenPaymentTerminal(msg.sender) + ][_projectId][fundingCycle.number] + _amount; + + // Amount must be within what is still distributable. + (uint256 _distributionLimitOf, uint256 _distributionLimitCurrencyOf) = IJBController3_1( + directory.controllerOf(_projectId) + ).fundAccessConstraintsStore().distributionLimitOf( + _projectId, + fundingCycle.configuration, + IJBSingleTokenPaymentTerminal(msg.sender), + IJBSingleTokenPaymentTerminal(msg.sender).token() + ); + + // Make sure the new used amount is within the distribution limit. + if (_newUsedDistributionLimitOf > _distributionLimitOf || _distributionLimitOf == 0) + revert DISTRIBUTION_AMOUNT_LIMIT_REACHED(); + + // Make sure the currencies match. + if (_currency != _distributionLimitCurrencyOf) revert CURRENCY_MISMATCH(); + + // Get a reference to the terminal's currency. + uint256 _balanceCurrency = IJBSingleTokenPaymentTerminal(msg.sender).currency(); + + // Convert the amount to the balance's currency. + distributedAmount = (_currency == _balanceCurrency) + ? _amount + : PRBMath.mulDiv( + _amount, + 10 ** _MAX_FIXED_POINT_FIDELITY, // Use _MAX_FIXED_POINT_FIDELITY to keep as much of the `_amount.value`'s fidelity as possible when converting. + prices.priceFor(_currency, _balanceCurrency, _MAX_FIXED_POINT_FIDELITY) + ); + + // The amount being distributed must be available. + if (distributedAmount > balanceOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId]) + revert INADEQUATE_PAYMENT_TERMINAL_STORE_BALANCE(); + + // Store the new amount. + usedDistributionLimitOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId][ + fundingCycle.number + ] = _newUsedDistributionLimitOf; + + // Removed the distributed funds from the project's token balance. + unchecked { + balanceOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId] = + balanceOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId] - + distributedAmount; + } + } + + /// @notice Records newly used allowance funds of a project. + /// @dev The msg.sender must be an IJBSingleTokenPaymentTerminal. + /// @param _projectId The ID of the project to use the allowance of. + /// @param _amount The amount to use from the allowance, as a fixed point number. + /// @param _currency The currency of the `_amount`. Must match the currency of the overflow allowance. + /// @return fundingCycle The funding cycle during which the overflow allowance is being used. + /// @return usedAmount The amount of terminal tokens used, as a fixed point number with the same amount of decimals as its relative terminal. + function recordUsedAllowanceOf( + uint256 _projectId, + uint256 _amount, + uint256 _currency + ) + external + override + nonReentrant + returns (JBFundingCycle memory fundingCycle, uint256 usedAmount) + { + // Get a reference to the project's current funding cycle. + fundingCycle = fundingCycleStore.currentOf(_projectId); + + // Get a reference to the new used overflow allowance for this funding cycle configuration. + uint256 _newUsedOverflowAllowanceOf = usedOverflowAllowanceOf[ + IJBSingleTokenPaymentTerminal(msg.sender) + ][_projectId][fundingCycle.configuration] + _amount; + + // There must be sufficient allowance available. + (uint256 _overflowAllowanceOf, uint256 _overflowAllowanceCurrency) = IJBController3_1( + directory.controllerOf(_projectId) + ).fundAccessConstraintsStore().overflowAllowanceOf( + _projectId, + fundingCycle.configuration, + IJBSingleTokenPaymentTerminal(msg.sender), + IJBSingleTokenPaymentTerminal(msg.sender).token() + ); + + // Make sure the new used amount is within the allowance. + if (_newUsedOverflowAllowanceOf > _overflowAllowanceOf || _overflowAllowanceOf == 0) + revert INADEQUATE_CONTROLLER_ALLOWANCE(); + + // Make sure the currencies match. + if (_currency != _overflowAllowanceCurrency) revert CURRENCY_MISMATCH(); + + // Get a reference to the terminal's currency. + uint256 _balanceCurrency = IJBSingleTokenPaymentTerminal(msg.sender).currency(); + + // Convert the amount to this store's terminal's token. + usedAmount = (_currency == _balanceCurrency) + ? _amount + : PRBMath.mulDiv( + _amount, + 10 ** _MAX_FIXED_POINT_FIDELITY, // Use _MAX_FIXED_POINT_FIDELITY to keep as much of the `_amount.value`'s fidelity as possible when converting. + prices.priceFor(_currency, _balanceCurrency, _MAX_FIXED_POINT_FIDELITY) + ); + + // The amount being distributed must be available in the overflow. + if ( + usedAmount > + _overflowDuring( + IJBSingleTokenPaymentTerminal(msg.sender), + _projectId, + fundingCycle, + _balanceCurrency + ) + ) revert INADEQUATE_PAYMENT_TERMINAL_STORE_BALANCE(); + + // Store the incremented value. + usedOverflowAllowanceOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId][ + fundingCycle.configuration + ] = _newUsedOverflowAllowanceOf; + + // Update the project's balance. + balanceOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId] = + balanceOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId] - + usedAmount; + } + + /// @notice Records newly added funds for the project. + /// @dev The msg.sender must be an IJBSingleTokenPaymentTerminal. + /// @param _projectId The ID of the project to which the funds being added belong. + /// @param _amount The amount of terminal tokens added, as a fixed point number with the same amount of decimals as its relative terminal. + function recordAddedBalanceFor(uint256 _projectId, uint256 _amount) external override { + // Increment the balance. + balanceOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId] = + balanceOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId] + + _amount; + } + + /// @notice Records the migration of funds from this store. + /// @dev The msg.sender must be an IJBSingleTokenPaymentTerminal. The amount returned is in terms of the msg.senders tokens. + /// @param _projectId The ID of the project being migrated. + /// @return balance The project's migrated balance, as a fixed point number with the same amount of decimals as its relative terminal. + function recordMigration( + uint256 _projectId + ) external override nonReentrant returns (uint256 balance) { + // Get a reference to the project's current funding cycle. + JBFundingCycle memory _fundingCycle = fundingCycleStore.currentOf(_projectId); + + // Migration must be allowed. + if (!_fundingCycle.terminalMigrationAllowed()) revert PAYMENT_TERMINAL_MIGRATION_NOT_ALLOWED(); + + // Return the current balance. + balance = balanceOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId]; + + // Set the balance to 0. + balanceOf[IJBSingleTokenPaymentTerminal(msg.sender)][_projectId] = 0; + } + + //*********************************************************************// + // --------------------- private helper functions -------------------- // + //*********************************************************************// + + /// @notice The amount of overflowed tokens from a terminal that can be reclaimed by the specified number of tokens when measured from the specified. + /// @dev If the project has an active funding cycle reconfiguration ballot, the project's ballot redemption rate is used. + /// @param _projectId The ID of the project to get the reclaimable overflow amount for. + /// @param _fundingCycle The funding cycle during which reclaimable overflow is being calculated. + /// @param _tokenCount The number of tokens to make the calculation with, as a fixed point number with 18 decimals. + /// @param _totalSupply The total supply of tokens to make the calculation with, as a fixed point number with 18 decimals. + /// @param _overflow The amount of overflow to make the calculation with. + /// @return The amount of overflowed tokens that can be reclaimed. + function _reclaimableOverflowDuring( + uint256 _projectId, + JBFundingCycle memory _fundingCycle, + uint256 _tokenCount, + uint256 _totalSupply, + uint256 _overflow + ) private view returns (uint256) { + // If the amount being redeemed is the total supply, return the rest of the overflow. + if (_tokenCount == _totalSupply) return _overflow; + + // Use the ballot redemption rate if the queued cycle is pending approval according to the previous funding cycle's ballot. + uint256 _redemptionRate = _fundingCycle.redemptionRate(); + + // If the redemption rate is 0, nothing is claimable. + if (_redemptionRate == 0) return 0; + + // Get a reference to the linear proportion. + uint256 _base = PRBMath.mulDiv(_overflow, _tokenCount, _totalSupply); + + // These conditions are all part of the same curve. Edge conditions are separated because fewer operation are necessary. + if (_redemptionRate == JBConstants.MAX_REDEMPTION_RATE) return _base; + + return + PRBMath.mulDiv( + _base, + _redemptionRate + + PRBMath.mulDiv( + _tokenCount, + JBConstants.MAX_REDEMPTION_RATE - _redemptionRate, + _totalSupply + ), + JBConstants.MAX_REDEMPTION_RATE + ); + } + + /// @notice Gets the amount that is overflowing when measured from the specified funding cycle. + /// @dev This amount changes as the value of the balance changes in relation to the currency being used to measure the distribution limit. + /// @param _terminal The terminal for which the overflow is being calculated. + /// @param _projectId The ID of the project to get overflow for. + /// @param _fundingCycle The ID of the funding cycle to base the overflow on. + /// @param _balanceCurrency The currency that the stored balance is expected to be in terms of. + /// @return overflow The overflow of funds, as a fixed point number with 18 decimals. + function _overflowDuring( + IJBSingleTokenPaymentTerminal _terminal, + uint256 _projectId, + JBFundingCycle memory _fundingCycle, + uint256 _balanceCurrency + ) private view returns (uint256) { + // Get the current balance of the project. + uint256 _balanceOf = balanceOf[_terminal][_projectId]; + + // If there's no balance, there's no overflow. + if (_balanceOf == 0) return 0; + + // Get a reference to the distribution limit during the funding cycle. + (uint256 _distributionLimit, uint256 _distributionLimitCurrency) = IJBController3_1( + directory.controllerOf(_projectId) + ).fundAccessConstraintsStore().distributionLimitOf( + _projectId, + _fundingCycle.configuration, + _terminal, + _terminal.token() + ); + + // Get a reference to the amount still distributable during the funding cycle. + uint256 _distributionLimitRemaining = _distributionLimit - + usedDistributionLimitOf[_terminal][_projectId][_fundingCycle.number]; + + // Convert the _distributionRemaining to be in terms of the provided currency. + if (_distributionLimitRemaining != 0 && _distributionLimitCurrency != _balanceCurrency) + _distributionLimitRemaining = PRBMath.mulDiv( + _distributionLimitRemaining, + 10 ** _MAX_FIXED_POINT_FIDELITY, // Use _MAX_FIXED_POINT_FIDELITY to keep as much of the `_amount.value`'s fidelity as possible when converting. + prices.priceFor(_distributionLimitCurrency, _balanceCurrency, _MAX_FIXED_POINT_FIDELITY) + ); + + // Overflow is the balance of this project minus the amount that can still be distributed. + unchecked { + return + _balanceOf > _distributionLimitRemaining ? _balanceOf - _distributionLimitRemaining : 0; + } + } + + /// @notice Gets the amount that is currently overflowing across all of a project's terminals. + /// @dev This amount changes as the value of the balances changes in relation to the currency being used to measure the project's distribution limits. + /// @param _projectId The ID of the project to get the total overflow for. + /// @param _decimals The number of decimals that the fixed point overflow should include. + /// @param _currency The currency that the overflow should be in terms of. + /// @return overflow The total overflow of a project's funds. + function _currentTotalOverflowOf( + uint256 _projectId, + uint256 _decimals, + uint256 _currency + ) private view returns (uint256) { + // Get a reference to the project's terminals. + IJBPaymentTerminal[] memory _terminals = directory.terminalsOf(_projectId); + + // Keep a reference to the ETH overflow across all terminals, as a fixed point number with 18 decimals. + uint256 _ethOverflow; + + // Add the current ETH overflow for each terminal. + for (uint256 _i; _i < _terminals.length; ) { + _ethOverflow = _ethOverflow + _terminals[_i].currentEthOverflowOf(_projectId); + unchecked { + ++_i; + } + } + + // Convert the ETH overflow to the specified currency if needed, maintaining a fixed point number with 18 decimals. + uint256 _totalOverflow18Decimal = _currency == JBCurrencies.ETH + ? _ethOverflow + : PRBMath.mulDiv(_ethOverflow, 10 ** 18, prices.priceFor(JBCurrencies.ETH, _currency, 18)); + + // Adjust the decimals of the fixed point number if needed to match the target decimals. + return + (_decimals == 18) + ? _totalOverflow18Decimal + : JBFixedPointNumber.adjustDecimals(_totalOverflow18Decimal, 18, _decimals); + } +} diff --git a/contracts/abstract/JBPayoutRedemptionPaymentTerminal3_2.sol b/contracts/abstract/JBPayoutRedemptionPaymentTerminal3_2.sol new file mode 100644 index 000000000..d59bc6ef7 --- /dev/null +++ b/contracts/abstract/JBPayoutRedemptionPaymentTerminal3_2.sol @@ -0,0 +1,1570 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; +import {IERC165} from '@openzeppelin/contracts/utils/introspection/IERC165.sol'; +import {ERC165Checker} from '@openzeppelin/contracts/utils/introspection/ERC165Checker.sol'; +import {PRBMath} from '@paulrberg/contracts/math/PRBMath.sol'; +import {JBFeeType} from './../enums/JBFeeType.sol'; +import {IJBAllowanceTerminal3_1} from './../interfaces/IJBAllowanceTerminal3_1.sol'; +import {IJBController} from './../interfaces/IJBController.sol'; +import {IJBDirectory} from './../interfaces/IJBDirectory.sol'; +import {IJBPayoutRedemptionPaymentTerminal3_2} from './../interfaces/IJBPayoutRedemptionPaymentTerminal3_2.sol'; +import {IJBSplitsStore} from './../interfaces/IJBSplitsStore.sol'; +import {IJBFeeGauge3_1} from './../interfaces/IJBFeeGauge3_1.sol'; +import {IJBOperatable} from './../interfaces/IJBOperatable.sol'; +import {IJBOperatorStore} from './../interfaces/IJBOperatorStore.sol'; +import {IJBPaymentTerminal} from './../interfaces/IJBPaymentTerminal.sol'; +import {IJBPayoutTerminal3_1} from './../interfaces/IJBPayoutTerminal3_1.sol'; +import {IJBPrices} from './../interfaces/IJBPrices.sol'; +import {IJBProjects} from './../interfaces/IJBProjects.sol'; +import {IJBRedemptionTerminal} from './../interfaces/IJBRedemptionTerminal.sol'; +import {IJBSingleTokenPaymentTerminalStore3_1_1} from './../interfaces/IJBSingleTokenPaymentTerminalStore3_1_1.sol'; +import {IJBSplitAllocator} from './../interfaces/IJBSplitAllocator.sol'; +import {JBConstants} from './../libraries/JBConstants.sol'; +import {JBCurrencies} from './../libraries/JBCurrencies.sol'; +import {JBFees} from './../libraries/JBFees.sol'; +import {JBFixedPointNumber} from './../libraries/JBFixedPointNumber.sol'; +import {JBFundingCycleMetadataResolver3_2} from './../libraries/JBFundingCycleMetadataResolver3_2.sol'; +import {JBOperations} from './../libraries/JBOperations.sol'; +import {JBTokens} from './../libraries/JBTokens.sol'; +import {JBDidRedeemData3_1_1} from './../structs/JBDidRedeemData3_1_1.sol'; +import {JBDidPayData3_1_1} from './../structs/JBDidPayData3_1_1.sol'; +import {JBFee} from './../structs/JBFee.sol'; +import {JBFundingCycle} from './../structs/JBFundingCycle.sol'; +import {JBPayDelegateAllocation3_1_1} from './../structs/JBPayDelegateAllocation3_1_1.sol'; +import {JBRedemptionDelegateAllocation3_1_1} from './../structs/JBRedemptionDelegateAllocation3_1_1.sol'; +import {JBSplit} from './../structs/JBSplit.sol'; +import {JBSplitAllocationData} from './../structs/JBSplitAllocationData.sol'; +import {JBTokenAmount} from './../structs/JBTokenAmount.sol'; +import {JBOperatable} from './JBOperatable.sol'; +import {JBSingleTokenPaymentTerminal} from './JBSingleTokenPaymentTerminal.sol'; + +/// @notice Generic terminal managing all inflows and outflows of funds into the protocol ecosystem. +abstract contract JBPayoutRedemptionPaymentTerminal3_2 is + JBSingleTokenPaymentTerminal, + JBOperatable, + Ownable, + IJBPayoutRedemptionPaymentTerminal3_2 +{ + // A library that parses the packed funding cycle metadata into a friendlier format. + using JBFundingCycleMetadataResolver3_2 for JBFundingCycle; + + //*********************************************************************// + // --------------------------- custom errors ------------------------- // + //*********************************************************************// + error FEE_TOO_HIGH(); + error INADEQUATE_DISTRIBUTION_AMOUNT(); + error INADEQUATE_RECLAIM_AMOUNT(); + error INADEQUATE_TOKEN_COUNT(); + error NO_MSG_VALUE_ALLOWED(); + error PAY_TO_ZERO_ADDRESS(); + error REDEEM_TO_ZERO_ADDRESS(); + error TERMINAL_TOKENS_INCOMPATIBLE(); + + //*********************************************************************// + // --------------------- internal stored constants ------------------- // + //*********************************************************************// + + /// @notice Maximum fee that can be set for a funding cycle configuration. + /// @dev Out of MAX_FEE (50_000_000 / 1_000_000_000). + uint256 internal constant _FEE_CAP = 50_000_000; + + /// @notice The fee beneficiary project ID is 1, as it should be the first project launched during the deployment process. + uint256 internal constant _FEE_BENEFICIARY_PROJECT_ID = 1; + + //*********************************************************************// + // --------------------- internal stored properties ------------------ // + //*********************************************************************// + + /// @notice Fees that are being held to be processed later. + /// @custom:param _projectId The ID of the project for which fees are being held. + mapping(uint256 => JBFee[]) internal _heldFeesOf; + + //*********************************************************************// + // ---------------- public immutable stored properties --------------- // + //*********************************************************************// + + /// @notice Mints ERC-721's that represent project ownership and transfers. + IJBProjects public immutable override projects; + + /// @notice The directory of terminals and controllers for projects. + IJBDirectory public immutable override directory; + + /// @notice The contract that stores splits for each project. + IJBSplitsStore public immutable override splitsStore; + + /// @notice The contract that exposes price feeds. + IJBPrices public immutable override prices; + + /// @notice The contract that stores and manages the terminal's data. + address public immutable override store; + + /// @notice The group that payout splits coming from this terminal are identified by. + uint256 public immutable override payoutSplitsGroup; + + //*********************************************************************// + // --------------------- public stored properties -------------------- // + //*********************************************************************// + + /// @notice The platform fee percent. + /// @dev Out of MAX_FEE (25_000_000 / 1_000_000_000) + uint256 public override fee = 25_000_000; // 2.5% + + /// @notice The data source that returns a discount to apply to a project's fee. + address public override feeGauge; + + /// @notice Addresses that can be paid towards from this terminal without incurring a fee. + /// @dev Only addresses that are considered to be contained within the ecosystem can be feeless. Funds sent outside the ecosystem may incur fees despite being stored as feeless. + /// @custom:param _address The address that can be paid toward. + mapping(address => bool) public override isFeelessAddress; + + //*********************************************************************// + // ------------------------- external views -------------------------- // + //*********************************************************************// + + /// @notice Gets the current overflowed amount in this terminal for a specified project, in terms of ETH. + /// @dev The current overflow is represented as a fixed point number with 18 decimals. + /// @param _projectId The ID of the project to get overflow for. + /// @return The current amount of ETH overflow that project has in this terminal, as a fixed point number with 18 decimals. + function currentEthOverflowOf( + uint256 _projectId + ) external view virtual override returns (uint256) { + // Get this terminal's current overflow. + uint256 _overflow = IJBSingleTokenPaymentTerminalStore3_1_1(store).currentOverflowOf( + this, + _projectId + ); + + // Adjust the decimals of the fixed point number if needed to have 18 decimals. + uint256 _adjustedOverflow = (decimals == 18) + ? _overflow + : JBFixedPointNumber.adjustDecimals(_overflow, decimals, 18); + + // Return the amount converted to ETH. + return + (currency == JBCurrencies.ETH) + ? _adjustedOverflow + : PRBMath.mulDiv( + _adjustedOverflow, + 10 ** decimals, + prices.priceFor(currency, JBCurrencies.ETH, decimals) + ); + } + + /// @notice The fees that are currently being held to be processed later for each project. + /// @param _projectId The ID of the project for which fees are being held. + /// @return An array of fees that are being held. + function heldFeesOf(uint256 _projectId) external view override returns (JBFee[] memory) { + return _heldFeesOf[_projectId]; + } + + //*********************************************************************// + // -------------------------- public views --------------------------- // + //*********************************************************************// + + /// @notice Indicates if this contract adheres to the specified interface. + /// @dev See {IERC165-supportsInterface}. + /// @param _interfaceId The ID of the interface to check for adherance to. + /// @return A flag indicating if the provided interface ID is supported. + function supportsInterface( + bytes4 _interfaceId + ) public view virtual override(JBSingleTokenPaymentTerminal, IERC165) returns (bool) { + return + _interfaceId == type(IJBPayoutRedemptionPaymentTerminal3_2).interfaceId || + _interfaceId == type(IJBPayoutTerminal3_1).interfaceId || + _interfaceId == type(IJBAllowanceTerminal3_1).interfaceId || + _interfaceId == type(IJBRedemptionTerminal).interfaceId || + _interfaceId == type(IJBOperatable).interfaceId || + super.supportsInterface(_interfaceId); + } + + //*********************************************************************// + // -------------------------- internal views ------------------------- // + //*********************************************************************// + + /// @notice Checks the balance of tokens in this contract. + /// @return The contract's balance. + function _balance() internal view virtual returns (uint256); + + //*********************************************************************// + // -------------------------- constructor ---------------------------- // + //*********************************************************************// + + /// @param _token The token that this terminal manages. + /// @param _decimals The number of decimals the token fixed point amounts are expected to have. + /// @param _currency The currency that this terminal's token adheres to for price feeds. + /// @param _payoutSplitsGroup The group that denotes payout splits from this terminal in the splits store. + /// @param _operatorStore A contract storing operator assignments. + /// @param _projects A contract which mints ERC-721's that represent project ownership and transfers. + /// @param _directory A contract storing directories of terminals and controllers for each project. + /// @param _splitsStore A contract that stores splits for each project. + /// @param _prices A contract that exposes price feeds. + /// @param _store A contract that stores the terminal's data. + /// @param _owner The address that will own this contract. + constructor( + // payable constructor save the gas used to check msg.value==0 + address _token, + uint256 _decimals, + uint256 _currency, + uint256 _payoutSplitsGroup, + IJBOperatorStore _operatorStore, + IJBProjects _projects, + IJBDirectory _directory, + IJBSplitsStore _splitsStore, + IJBPrices _prices, + address _store, + address _owner + ) + payable + JBSingleTokenPaymentTerminal(_token, _decimals, _currency) + JBOperatable(_operatorStore) + { + payoutSplitsGroup = _payoutSplitsGroup; + projects = _projects; + directory = _directory; + splitsStore = _splitsStore; + prices = _prices; + store = _store; + + transferOwnership(_owner); + } + + //*********************************************************************// + // ---------------------- external transactions ---------------------- // + //*********************************************************************// + + /// @notice Contribute tokens to a project. + /// @param _projectId The ID of the project being paid. + /// @param _amount The amount of terminal tokens being received, as a fixed point number with the same amount of decimals as this terminal. If this terminal's token is ETH, this is ignored and msg.value is used in its place. + /// @param _token The token being paid. This terminal ignores this property since it only manages one token. + /// @param _beneficiary The address to mint tokens for and pass along to the funding cycle's data source and delegate. + /// @param _minReturnedTokens The minimum number of project tokens expected in return, as a fixed point number with the same amount of decimals as this terminal. + /// @param _preferClaimedTokens A flag indicating whether the request prefers to mint project tokens into the beneficiaries wallet rather than leaving them unclaimed. This is only possible if the project has an attached token contract. Leaving them unclaimed saves gas. + /// @param _memo A memo to pass along to the emitted event, and passed along the the funding cycle's data source and delegate. A data source can alter the memo before emitting in the event and forwarding to the delegate. + /// @param _metadata Bytes to send along to the data source, delegate, and emitted event, if provided. + /// @return The number of tokens minted for the beneficiary, as a fixed point number with 18 decimals. + function pay( + uint256 _projectId, + uint256 _amount, + address _token, + address _beneficiary, + uint256 _minReturnedTokens, + bool _preferClaimedTokens, + string calldata _memo, + bytes calldata _metadata + ) external payable virtual override returns (uint256) { + _token; // Prevents unused var compiler and natspec complaints. + + // ETH shouldn't be sent if this terminal's token isn't ETH. + if (token != JBTokens.ETH) { + if (msg.value != 0) revert NO_MSG_VALUE_ALLOWED(); + + // Get a reference to the balance before receiving tokens. + uint256 _balanceBefore = _balance(); + + // Transfer tokens to this terminal from the msg sender. + _transferFrom(msg.sender, payable(address(this)), _amount); + + // The amount should reflect the change in balance. + _amount = _balance() - _balanceBefore; + } + // If this terminal's token is ETH, override _amount with msg.value. + else _amount = msg.value; + + return + _pay( + _amount, + msg.sender, + _projectId, + _beneficiary, + _minReturnedTokens, + _preferClaimedTokens, + _memo, + _metadata + ); + } + + /// @notice Holders can redeem their tokens to claim the project's overflowed tokens, or to trigger rules determined by the project's current funding cycle's data source. + /// @dev Only a token holder or a designated operator can redeem its tokens. + /// @param _holder The account to redeem tokens for. + /// @param _projectId The ID of the project to which the tokens being redeemed belong. + /// @param _tokenCount The number of project tokens to redeem, as a fixed point number with 18 decimals. + /// @param _token The token being reclaimed. This terminal ignores this property since it only manages one token. + /// @param _minReturnedTokens The minimum amount of terminal tokens expected in return, as a fixed point number with the same amount of decimals as the terminal. + /// @param _beneficiary The address to send the terminal tokens to. + /// @param _memo A memo to pass along to the emitted event. + /// @param _metadata Bytes to send along to the data source, delegate, and emitted event, if provided. + /// @return reclaimAmount The amount of terminal tokens that the project tokens were redeemed for, as a fixed point number with 18 decimals. + function redeemTokensOf( + address _holder, + uint256 _projectId, + uint256 _tokenCount, + address _token, + uint256 _minReturnedTokens, + address payable _beneficiary, + string memory _memo, + bytes memory _metadata + ) + external + virtual + override + requirePermission(_holder, _projectId, JBOperations.REDEEM) + returns (uint256 reclaimAmount) + { + _token; // Prevents unused var compiler and natspec complaints. + + return + _redeemTokensOf( + _holder, + _projectId, + _tokenCount, + _minReturnedTokens, + _beneficiary, + _memo, + _metadata + ); + } + + /// @notice Distributes payouts for a project with the distribution limit of its current funding cycle. + /// @dev Payouts are sent to the preprogrammed splits. Any leftover is sent to the project's owner. + /// @dev Anyone can distribute payouts on a project's behalf. The project can preconfigure a wildcard split that is used to send funds to msg.sender. This can be used to incentivize calling this function. + /// @dev All funds distributed outside of this contract or any feeless terminals incure the protocol fee. + /// @param _projectId The ID of the project having its payouts distributed. + /// @param _amount The amount of terminal tokens to distribute, as a fixed point number with same number of decimals as this terminal. + /// @param _currency The expected currency of the amount being distributed. Must match the project's current funding cycle's distribution limit currency. + /// @param _token The token being distributed. This terminal ignores this property since it only manages one token. + /// @param _minReturnedTokens The minimum number of terminal tokens that the `_amount` should be valued at in terms of this terminal's currency, as a fixed point number with the same number of decimals as this terminal. + /// @param _metadata Bytes to send along to the emitted event, if provided. + /// @return netLeftoverDistributionAmount The amount that was sent to the project owner, as a fixed point number with the same amount of decimals as this terminal. + function distributePayoutsOf( + uint256 _projectId, + uint256 _amount, + uint256 _currency, + address _token, + uint256 _minReturnedTokens, + bytes calldata _metadata + ) external virtual override returns (uint256 netLeftoverDistributionAmount) { + _token; // Prevents unused var compiler and natspec complaints. + + return _distributePayoutsOf(_projectId, _amount, _currency, _minReturnedTokens, _metadata); + } + + /// @notice Allows a project to send funds from its overflow up to the preconfigured allowance. + /// @dev Only a project's owner or a designated operator can use its allowance. + /// @dev Incurs the protocol fee. + /// @param _projectId The ID of the project to use the allowance of. + /// @param _amount The amount of terminal tokens to use from this project's current allowance, as a fixed point number with the same amount of decimals as this terminal. + /// @param _currency The expected currency of the amount being distributed. Must match the project's current funding cycle's overflow allowance currency. + /// @param _token The token being distributed. This terminal ignores this property since it only manages one token. + /// @param _minReturnedTokens The minimum number of tokens that the `_amount` should be valued at in terms of this terminal's currency, as a fixed point number with 18 decimals. + /// @param _beneficiary The address to send the funds to. + /// @param _memo A memo to pass along to the emitted event. + /// @param _metadata Bytes to send along to the emitted event, if provided. + /// @return netDistributedAmount The amount of tokens that was distributed to the beneficiary, as a fixed point number with the same amount of decimals as the terminal. + function useAllowanceOf( + uint256 _projectId, + uint256 _amount, + uint256 _currency, + address _token, + uint256 _minReturnedTokens, + address payable _beneficiary, + string memory _memo, + bytes calldata _metadata + ) + external + virtual + override + requirePermission(projects.ownerOf(_projectId), _projectId, JBOperations.USE_ALLOWANCE) + returns (uint256 netDistributedAmount) + { + _token; // Prevents unused var compiler and natspec complaints. + + return + _useAllowanceOf( + _projectId, + _amount, + _currency, + _minReturnedTokens, + _beneficiary, + _memo, + _metadata + ); + } + + /// @notice Allows a project owner to migrate its funds and operations to a new terminal that accepts the same token type. + /// @dev Only a project's owner or a designated operator can migrate it. + /// @param _projectId The ID of the project being migrated. + /// @param _to The terminal contract that will gain the project's funds. + /// @return balance The amount of funds that were migrated, as a fixed point number with the same amount of decimals as this terminal. + function migrate( + uint256 _projectId, + IJBPaymentTerminal _to + ) + external + virtual + override + requirePermission(projects.ownerOf(_projectId), _projectId, JBOperations.MIGRATE_TERMINAL) + returns (uint256 balance) + { + // The terminal being migrated to must accept the same token as this terminal. + if (!_to.acceptsToken(token, _projectId)) revert TERMINAL_TOKENS_INCOMPATIBLE(); + + // Record the migration in the store. + balance = IJBSingleTokenPaymentTerminalStore3_1_1(store).recordMigration(_projectId); + + // Transfer the balance if needed. + if (balance != 0) { + // Trigger any inherited pre-transfer logic. + _beforeTransferTo(address(_to), balance); + + // If this terminal's token is ETH, send it in msg.value. + uint256 _payableValue = token == JBTokens.ETH ? balance : 0; + + // Withdraw the balance to transfer to the new terminal; + _to.addToBalanceOf{value: _payableValue}(_projectId, balance, token, '', bytes('')); + } + + emit Migrate(_projectId, _to, balance, msg.sender); + } + + /// @notice Receives funds belonging to the specified project. + /// @param _projectId The ID of the project to which the funds received belong. + /// @param _amount The amount of tokens to add, as a fixed point number with the same number of decimals as this terminal. If this is an ETH terminal, this is ignored and msg.value is used instead. + /// @param _token The token being paid. This terminal ignores this property since it only manages one currency. + /// @param _memo A memo to pass along to the emitted event. + /// @param _metadata Extra data to pass along to the emitted event. + function addToBalanceOf( + uint256 _projectId, + uint256 _amount, + address _token, + string calldata _memo, + bytes calldata _metadata + ) external payable virtual override { + // Do not refund held fees by default. + addToBalanceOf(_projectId, _amount, _token, false, _memo, _metadata); + } + + /// @notice Process any fees that are being held for the project. + /// @dev Only a project owner, an operator, or the contract's owner can process held fees. + /// @param _projectId The ID of the project whos held fees should be processed. + function processFees( + uint256 _projectId + ) + external + virtual + override + requirePermissionAllowingOverride( + projects.ownerOf(_projectId), + _projectId, + JBOperations.PROCESS_FEES, + msg.sender == owner() + ) + { + // Get a reference to the project's held fees. + JBFee[] memory _heldFees = _heldFeesOf[_projectId]; + + // Delete the held fees. + delete _heldFeesOf[_projectId]; + + // Push array length in stack + uint256 _heldFeeLength = _heldFees.length; + + // Keep a reference to the amount. + uint256 _amount; + + // Process each fee. + for (uint256 _i; _i < _heldFeeLength; ) { + // Get the fee amount. + _amount = ( + _heldFees[_i].fee == 0 || _heldFees[_i].feeDiscount == JBConstants.MAX_FEE_DISCOUNT + ? 0 + : JBFees.feeIn(_heldFees[_i].amount, _heldFees[_i].fee, _heldFees[_i].feeDiscount) + ); + + // Process the fee. + _processFee(_amount, _heldFees[_i].beneficiary, _projectId); + + emit ProcessFee(_projectId, _amount, true, _heldFees[_i].beneficiary, msg.sender); + + unchecked { + ++_i; + } + } + } + + /// @notice Allows the fee to be updated. + /// @dev Only the owner of this contract can change the fee. + /// @param _fee The new fee, out of MAX_FEE. + function setFee(uint256 _fee) external virtual override onlyOwner { + // The provided fee must be within the max. + if (_fee > _FEE_CAP) revert FEE_TOO_HIGH(); + + // Store the new fee. + fee = _fee; + + emit SetFee(_fee, msg.sender); + } + + /// @notice Allows the fee gauge to be updated. + /// @dev Only the owner of this contract can change the fee gauge. + /// @param _feeGauge The new fee gauge. + function setFeeGauge(address _feeGauge) external virtual override onlyOwner { + // Store the new fee gauge. + feeGauge = _feeGauge; + + emit SetFeeGauge(_feeGauge, msg.sender); + } + + /// @notice Sets whether projects operating on this terminal can pay towards the specified address without incurring a fee. + /// @dev Only the owner of this contract can set addresses as feeless. + /// @param _address The address that can be paid towards while still bypassing fees. + /// @param _flag A flag indicating whether the terminal should be feeless or not. + function setFeelessAddress(address _address, bool _flag) external virtual override onlyOwner { + // Set the flag value. + isFeelessAddress[_address] = _flag; + + emit SetFeelessAddress(_address, _flag, msg.sender); + } + + //*********************************************************************// + // ----------------------- public transactions ----------------------- // + //*********************************************************************// + + /// @notice Receives funds belonging to the specified project. + /// @param _projectId The ID of the project to which the funds received belong. + /// @param _amount The amount of tokens to add, as a fixed point number with the same number of decimals as this terminal. If this is an ETH terminal, this is ignored and msg.value is used instead. + /// @param _token The token being paid. This terminal ignores this property since it only manages one currency. + /// @param _shouldRefundHeldFees A flag indicating if held fees should be refunded based on the amount being added. + /// @param _memo A memo to pass along to the emitted event. + /// @param _metadata Extra data to pass along to the emitted event. + function addToBalanceOf( + uint256 _projectId, + uint256 _amount, + address _token, + bool _shouldRefundHeldFees, + string calldata _memo, + bytes calldata _metadata + ) public payable virtual override { + _token; // Prevents unused var compiler and natspec complaints. + + // If this terminal's token isn't ETH, make sure no msg.value was sent, then transfer the tokens in from msg.sender. + if (token != JBTokens.ETH) { + // Amount must be greater than 0. + if (msg.value != 0) revert NO_MSG_VALUE_ALLOWED(); + + // Get a reference to the balance before receiving tokens. + uint256 _balanceBefore = _balance(); + + // Transfer tokens to this terminal from the msg sender. + _transferFrom(msg.sender, payable(address(this)), _amount); + + // The amount should reflect the change in balance. + _amount = _balance() - _balanceBefore; + } + // If the terminal's token is ETH, override `_amount` with msg.value. + else _amount = msg.value; + + // Add to balance. + _addToBalanceOf(_projectId, _amount, _shouldRefundHeldFees, _memo, _metadata); + } + + //*********************************************************************// + // ---------------------- internal transactions ---------------------- // + //*********************************************************************// + + /// @notice Transfers tokens. + /// @param _from The address from which the transfer should originate. + /// @param _to The address to which the transfer should go. + /// @param _amount The amount of the transfer, as a fixed point number with the same number of decimals as this terminal. + function _transferFrom(address _from, address payable _to, uint256 _amount) internal virtual { + _from; // Prevents unused var compiler and natspec complaints. + _to; // Prevents unused var compiler and natspec complaints. + _amount; // Prevents unused var compiler and natspec complaints. + } + + /// @notice Logic to be triggered before transferring tokens from this terminal. + /// @param _to The address to which the transfer is going. + /// @param _amount The amount of the transfer, as a fixed point number with the same number of decimals as this terminal. + function _beforeTransferTo(address _to, uint256 _amount) internal virtual { + _to; // Prevents unused var compiler and natspec complaints. + _amount; // Prevents unused var compiler and natspec complaints. + } + + /// @notice Logic to be triggered if a transfer should be undone + /// @param _to The address to which the transfer went. + /// @param _amount The amount of the transfer, as a fixed point number with the same number of decimals as this terminal. + function _cancelTransferTo(address _to, uint256 _amount) internal virtual { + _to; // Prevents unused var compiler and natspec complaints. + _amount; // Prevents unused var compiler and natspec complaints. + } + + /// @notice Holders can redeem their tokens to claim the project's overflowed tokens, or to trigger rules determined by the project's current funding cycle's data source. + /// @dev Only a token holder or a designated operator can redeem its tokens. + /// @param _holder The account to redeem tokens for. + /// @param _projectId The ID of the project to which the tokens being redeemed belong. + /// @param _tokenCount The number of project tokens to redeem, as a fixed point number with 18 decimals. + /// @param _minReturnedTokens The minimum amount of terminal tokens expected in return, as a fixed point number with the same amount of decimals as the terminal. + /// @param _beneficiary The address to send the terminal tokens to. + /// @param _memo A memo to pass along to the emitted event. + /// @param _metadata Bytes to send along to the data source, delegate, and emitted event, if provided. + /// @return reclaimAmount The amount of terminal tokens that the project tokens were redeemed for, as a fixed point number with 18 decimals. + function _redeemTokensOf( + address _holder, + uint256 _projectId, + uint256 _tokenCount, + uint256 _minReturnedTokens, + address payable _beneficiary, + string memory _memo, + bytes memory _metadata + ) internal returns (uint256 reclaimAmount) { + // Can't send reclaimed funds to the zero address. + if (_beneficiary == address(0)) revert REDEEM_TO_ZERO_ADDRESS(); + + // Define variables that will be needed outside the scoped section below. + // Keep a reference to the funding cycle during which the redemption is being made. + JBFundingCycle memory _fundingCycle; + + // Scoped section prevents stack too deep. `_feeEligibleDistributionAmount`, `_feeDiscount` and `_feePercent` only used within scope. + { + // Keep a reference to the amount being reclaimed that should have fees withheld from. + uint256 _feeEligibleDistributionAmount; + + // Keep a reference to the amount of discount to apply to the fee. + uint256 _feeDiscount; + + // Keep a reference to the fee. + uint256 _feePercent = fee; + + // Scoped section prevents stack too deep. `_delegateAllocations` only used within scope. + { + JBRedemptionDelegateAllocation3_1_1[] memory _delegateAllocations; + + // Record the redemption. + ( + _fundingCycle, + reclaimAmount, + _delegateAllocations, + _memo + ) = IJBSingleTokenPaymentTerminalStore3_1_1(store).recordRedemptionFor( + _holder, + _projectId, + _tokenCount, + _memo, + _metadata + ); + + // Set the reference to the fee discount to apply. No fee if the beneficiary is feeless or if the redemption rate is at its max. + _feeDiscount = isFeelessAddress[_beneficiary] || + (_fundingCycle.redemptionRate() == JBConstants.MAX_REDEMPTION_RATE && + _fundingCycle.ballotRedemptionRate() == JBConstants.MAX_REDEMPTION_RATE) || + _feePercent == 0 + ? JBConstants.MAX_FEE_DISCOUNT + : _currentFeeDiscount(_projectId, JBFeeType.REDEMPTION); + + // The amount being reclaimed must be at least as much as was expected. + if (reclaimAmount < _minReturnedTokens) revert INADEQUATE_RECLAIM_AMOUNT(); + + // Burn the project tokens. + if (_tokenCount != 0) + IJBController(directory.controllerOf(_projectId)).burnTokensOf( + _holder, + _projectId, + _tokenCount, + '', + false + ); + + // If delegate allocations were specified by the data source, fulfill them. + if (_delegateAllocations.length != 0) { + JBDidRedeemData3_1_1 memory _data = JBDidRedeemData3_1_1( + _holder, + _projectId, + _fundingCycle.configuration, + _tokenCount, + JBTokenAmount(token, reclaimAmount, decimals, currency), + JBTokenAmount(token, 0, decimals, currency), + _beneficiary, + _memo, + bytes(''), + _metadata + ); + + // Keep a reference to the allocation. + JBRedemptionDelegateAllocation3_1_1 memory _delegateAllocation; + + // Keep a reference to the fee. + uint256 _delegatedAmountFee; + + // Keep a reference to the number of allocations. + uint256 _numDelegates = _delegateAllocations.length; + + for (uint256 _i; _i < _numDelegates; ) { + // Get a reference to the delegate being iterated on. + _delegateAllocation = _delegateAllocations[_i]; + + // Get the fee for the delegated amount. + _delegatedAmountFee = _feePercent == 0 + ? 0 + : JBFees.feeIn(_delegateAllocation.amount, _feePercent, _feeDiscount); + + // Add the delegated amount to the amount eligible for having a fee taken. + if (_delegatedAmountFee != 0) { + _feeEligibleDistributionAmount += _delegateAllocation.amount; + _delegateAllocation.amount -= _delegatedAmountFee; + } + + // Trigger any inherited pre-transfer logic. + _beforeTransferTo(address(_delegateAllocation.delegate), _delegateAllocation.amount); + + // Pass the correct token forwardedAmount to the delegate + _data.forwardedAmount.value = _delegateAllocation.amount; + + // Pass the correct metadata from the data source. + _data.dataSourceMetadata = _delegateAllocation.metadata; + + _delegateAllocation.delegate.didRedeem{ + value: token == JBTokens.ETH ? _delegateAllocation.amount : 0 + }(_data); + + emit DelegateDidRedeem( + _delegateAllocation.delegate, + _data, + _delegateAllocation.amount, + _delegatedAmountFee, + msg.sender + ); + + unchecked { + ++_i; + } + } + } + } + + // Send the reclaimed funds to the beneficiary. + if (reclaimAmount != 0) { + // Get the fee for the reclaimed amount. + uint256 _reclaimAmountFee = _feeDiscount == JBConstants.MAX_FEE_DISCOUNT + ? 0 + : JBFees.feeIn(reclaimAmount, _feePercent, _feeDiscount); + + if (_reclaimAmountFee != 0) { + _feeEligibleDistributionAmount += reclaimAmount; + reclaimAmount -= _reclaimAmountFee; + } + + // Subtract the fee from the reclaim amount. + if (reclaimAmount != 0) _transferFrom(address(this), _beneficiary, reclaimAmount); + } + + // Take the fee from all outbound reclaimations. + _feeEligibleDistributionAmount != 0 + ? _takeFeeFrom( + _projectId, + false, + _feeEligibleDistributionAmount, + _feePercent, + _beneficiary, + _feeDiscount + ) + : 0; + } + + emit RedeemTokens( + _fundingCycle.configuration, + _fundingCycle.number, + _projectId, + _holder, + _beneficiary, + _tokenCount, + reclaimAmount, + _memo, + _metadata, + msg.sender + ); + } + + /// @notice Distributes payouts for a project with the distribution limit of its current funding cycle. + /// @dev Payouts are sent to the preprogrammed splits. Any leftover is sent to the project's owner. + /// @dev Anyone can distribute payouts on a project's behalf. The project can preconfigure a wildcard split that is used to send funds to msg.sender. This can be used to incentivize calling this function. + /// @dev All funds distributed outside of this contract or any feeless terminals incure the protocol fee. + /// @param _projectId The ID of the project having its payouts distributed. + /// @param _amount The amount of terminal tokens to distribute, as a fixed point number with same number of decimals as this terminal. + /// @param _currency The expected currency of the amount being distributed. Must match the project's current funding cycle's distribution limit currency. + /// @param _minReturnedTokens The minimum number of terminal tokens that the `_amount` should be valued at in terms of this terminal's currency, as a fixed point number with the same number of decimals as this terminal. + /// @param _metadata Bytes to send along to the emitted event, if provided. + /// @return netLeftoverDistributionAmount The amount that was sent to the project owner, as a fixed point number with the same amount of decimals as this terminal. + function _distributePayoutsOf( + uint256 _projectId, + uint256 _amount, + uint256 _currency, + uint256 _minReturnedTokens, + bytes calldata _metadata + ) internal returns (uint256 netLeftoverDistributionAmount) { + // Record the distribution. + ( + JBFundingCycle memory _fundingCycle, + uint256 _distributedAmount + ) = IJBSingleTokenPaymentTerminalStore3_1_1(store).recordDistributionFor( + _projectId, + _amount, + _currency + ); + + // The amount being distributed must be at least as much as was expected. + if (_distributedAmount < _minReturnedTokens) revert INADEQUATE_DISTRIBUTION_AMOUNT(); + + // Get a reference to the project owner, which will receive tokens from paying the platform fee + // and receive any extra distributable funds not allocated to payout splits. + address payable _projectOwner = payable(projects.ownerOf(_projectId)); + + // Define variables that will be needed outside the scoped section below. + // Keep a reference to the fee amount that was paid. + uint256 _feeTaken; + + // Scoped section prevents stack too deep. `_feePercent`, `_feeDiscount`, `_feeEligibleDistributionAmount`, and `_leftoverDistributionAmount` only used within scope. + { + // Keep a reference to the fee. + uint256 _feePercent = fee; + + // Get the amount of discount that should be applied to any fees taken. + // If the fee is zero, set the discount to 100% for convenience. + uint256 _feeDiscount = _feePercent == 0 + ? JBConstants.MAX_FEE_DISCOUNT + : _currentFeeDiscount(_projectId, JBFeeType.PAYOUT); + + // The amount distributed that is eligible for incurring fees. + uint256 _feeEligibleDistributionAmount; + + // The amount leftover after distributing to the splits. + uint256 _leftoverDistributionAmount; + + // Payout to splits and get a reference to the leftover transfer amount after all splits have been paid. + // Also get a reference to the amount that was distributed to splits from which fees should be taken. + (_leftoverDistributionAmount, _feeEligibleDistributionAmount) = _distributeToPayoutSplitsOf( + _projectId, + _fundingCycle.configuration, + payoutSplitsGroup, + _distributedAmount, + _feePercent, + _feeDiscount + ); + + if (_feeDiscount != JBConstants.MAX_FEE_DISCOUNT) { + // Leftover distribution amount is also eligible for a fee since the funds are going out of the ecosystem to _beneficiary. + unchecked { + _feeEligibleDistributionAmount += _leftoverDistributionAmount; + } + } + + // Take the fee. + _feeTaken = _feeEligibleDistributionAmount != 0 + ? _takeFeeFrom( + _projectId, + _fundingCycle.shouldHoldFees(), + _feeEligibleDistributionAmount, + _feePercent, + _projectOwner, + _feeDiscount + ) + : 0; + + // Transfer any remaining balance to the project owner and update returned leftover accordingly. + if (_leftoverDistributionAmount != 0) { + // Subtract the fee from the net leftover amount. + netLeftoverDistributionAmount = + _leftoverDistributionAmount - + ( + _feeDiscount == JBConstants.MAX_FEE_DISCOUNT + ? 0 + : JBFees.feeIn(_leftoverDistributionAmount, _feePercent, _feeDiscount) + ); + + // Transfer the amount to the project owner. + _transferFrom(address(this), _projectOwner, netLeftoverDistributionAmount); + } + } + + emit DistributePayouts( + _fundingCycle.configuration, + _fundingCycle.number, + _projectId, + _projectOwner, + _amount, + _distributedAmount, + _feeTaken, + netLeftoverDistributionAmount, + _metadata, + msg.sender + ); + } + + /// @notice Allows a project to send funds from its overflow up to the preconfigured allowance. + /// @dev Only a project's owner or a designated operator can use its allowance. + /// @dev Incurs the protocol fee. + /// @param _projectId The ID of the project to use the allowance of. + /// @param _amount The amount of terminal tokens to use from this project's current allowance, as a fixed point number with the same amount of decimals as this terminal. + /// @param _currency The expected currency of the amount being distributed. Must match the project's current funding cycle's overflow allowance currency. + /// @param _minReturnedTokens The minimum number of tokens that the `_amount` should be valued at in terms of this terminal's currency, as a fixed point number with 18 decimals. + /// @param _beneficiary The address to send the funds to. + /// @param _memo A memo to pass along to the emitted event. + /// @param _metadata Bytes to send along to the emitted event, if provided. + /// @return netDistributedAmount The amount of tokens that was distributed to the beneficiary, as a fixed point number with the same amount of decimals as the terminal. + function _useAllowanceOf( + uint256 _projectId, + uint256 _amount, + uint256 _currency, + uint256 _minReturnedTokens, + address payable _beneficiary, + string memory _memo, + bytes calldata _metadata + ) internal returns (uint256 netDistributedAmount) { + // Record the use of the allowance. + ( + JBFundingCycle memory _fundingCycle, + uint256 _distributedAmount + ) = IJBSingleTokenPaymentTerminalStore3_1_1(store).recordUsedAllowanceOf( + _projectId, + _amount, + _currency + ); + + // The amount being withdrawn must be at least as much as was expected. + if (_distributedAmount < _minReturnedTokens) revert INADEQUATE_DISTRIBUTION_AMOUNT(); + + // Scoped section prevents stack too deep. `_fee`, `_projectOwner`, `_feeDiscount`, and `_netAmount` only used within scope. + { + // Keep a reference to the fee amount that was paid. + uint256 _feeTaken; + + // Keep a reference to the fee. + uint256 _feePercent = fee; + + // Get a reference to the project owner, which will receive tokens from paying the platform fee. + address _projectOwner = projects.ownerOf(_projectId); + + // Get the amount of discount that should be applied to any fees taken. + // If the fee is zero or if the fee is being used by an address that doesn't incur fees, set the discount to 100% for convenience. + uint256 _feeDiscount = _feePercent == 0 || isFeelessAddress[msg.sender] + ? JBConstants.MAX_FEE_DISCOUNT + : _currentFeeDiscount(_projectId, JBFeeType.ALLOWANCE); + + // Take a fee from the `_distributedAmount`, if needed. + _feeTaken = _feeDiscount == JBConstants.MAX_FEE_DISCOUNT + ? 0 + : _takeFeeFrom( + _projectId, + _fundingCycle.shouldHoldFees(), + _distributedAmount, + _feePercent, + _projectOwner, + _feeDiscount + ); + + unchecked { + // The net amount is the withdrawn amount without the fee. + netDistributedAmount = _distributedAmount - _feeTaken; + } + + // Transfer any remaining balance to the beneficiary. + if (netDistributedAmount != 0) + _transferFrom(address(this), _beneficiary, netDistributedAmount); + } + + emit UseAllowance( + _fundingCycle.configuration, + _fundingCycle.number, + _projectId, + _beneficiary, + _amount, + _distributedAmount, + netDistributedAmount, + _memo, + _metadata, + msg.sender + ); + } + + /// @notice Pays out splits for a project's funding cycle configuration. + /// @param _projectId The ID of the project for which payout splits are being distributed. + /// @param _domain The domain of the splits to distribute the payout between. + /// @param _group The group of the splits to distribute the payout between. + /// @param _amount The total amount being distributed, as a fixed point number with the same number of decimals as this terminal. + /// @param _feeDiscount The amount of discount to apply to the fee, out of the MAX_FEE. + /// @param _feePercent The percent of fees to take, out of MAX_FEE. + /// @return If the leftover amount if the splits don't add up to 100%. + /// @return feeEligibleDistributionAmount The total amount of distributions that are eligible to have fees taken from. + function _distributeToPayoutSplitsOf( + uint256 _projectId, + uint256 _domain, + uint256 _group, + uint256 _amount, + uint256 _feePercent, + uint256 _feeDiscount + ) internal returns (uint256, uint256 feeEligibleDistributionAmount) { + // The total percentage available to split + uint256 _leftoverPercentage = JBConstants.SPLITS_TOTAL_PERCENT; + + // Get a reference to the project's payout splits. + JBSplit[] memory _splits = splitsStore.splitsOf(_projectId, _domain, _group); + + // Keep a reference to the split being iterated on. + JBSplit memory _split; + + // Transfer between all splits. + for (uint256 _i; _i < _splits.length; ) { + // Get a reference to the split being iterated on. + _split = _splits[_i]; + + // The amount to send towards the split. + uint256 _payoutAmount = PRBMath.mulDiv(_amount, _split.percent, _leftoverPercentage); + + // The payout amount substracting any applicable incurred fees. + uint256 _netPayoutAmount = _distributeToPayoutSplit( + _split, + _projectId, + _group, + _payoutAmount, + _feePercent, + _feeDiscount + ); + + // If the split allocator is set as feeless, this distribution is not eligible for a fee. + if (_netPayoutAmount != 0 && _netPayoutAmount != _payoutAmount) + feeEligibleDistributionAmount += _payoutAmount; + + if (_payoutAmount != 0) { + // Subtract from the amount to be sent to the beneficiary. + unchecked { + _amount -= _payoutAmount; + } + } + + unchecked { + // Decrement the leftover percentage. + _leftoverPercentage -= _split.percent; + } + + emit DistributeToPayoutSplit( + _projectId, + _domain, + _group, + _split, + _payoutAmount, + _netPayoutAmount, + msg.sender + ); + + unchecked { + ++_i; + } + } + + return (_amount, feeEligibleDistributionAmount); + } + + /// @notice Pays out a split for a project's funding cycle configuration. + /// @param _split The split to distribute payouts to. + /// @param _amount The total amount being distributed to the split, as a fixed point number with the same number of decimals as this terminal. + /// @param _feePercent The percent of fees to take, out of MAX_FEE. + /// @param _feeDiscount The amount of discount to apply to the fee, out of the MAX_FEE. + /// @return netPayoutAmount The amount sent to the split after subtracting fees. + function _distributeToPayoutSplit( + JBSplit memory _split, + uint256 _projectId, + uint256 _group, + uint256 _amount, + uint256 _feePercent, + uint256 _feeDiscount + ) internal returns (uint256 netPayoutAmount) { + // By default, the net payout amount is the full amount. This will be adjusted if fees are taken. + netPayoutAmount = _amount; + + // If there's an allocator set, transfer to its `allocate` function. + if (_split.allocator != IJBSplitAllocator(address(0))) { + // This distribution is eligible for a fee since the funds are leaving this contract and the allocator isn't listed as feeless. + if ( + _feeDiscount != JBConstants.MAX_FEE_DISCOUNT && !isFeelessAddress[address(_split.allocator)] + ) { + unchecked { + netPayoutAmount -= JBFees.feeIn(_amount, _feePercent, _feeDiscount); + } + } + + // Trigger any inherited pre-transfer logic. + _beforeTransferTo(address(_split.allocator), netPayoutAmount); + + // Create the data to send to the allocator. + JBSplitAllocationData memory _data = JBSplitAllocationData( + token, + netPayoutAmount, + decimals, + _projectId, + _group, + _split + ); + + // Trigger the allocator's `allocate` function. + bytes memory _reason; + + if ( + ERC165Checker.supportsInterface( + address(_split.allocator), + type(IJBSplitAllocator).interfaceId + ) + ) + // If this terminal's token is ETH, send it in msg.value. + try + _split.allocator.allocate{value: token == JBTokens.ETH ? netPayoutAmount : 0}(_data) + {} catch (bytes memory __reason) { + _reason = __reason.length == 0 ? abi.encode('Allocate fail') : __reason; + } + else { + _reason = abi.encode('IERC165 fail'); + } + + if (_reason.length != 0) { + // Revert the payout. + _revertTransferFrom(_projectId, address(_split.allocator), netPayoutAmount, _amount); + + // Set the net payout amount to 0 to signal the reversion. + netPayoutAmount = 0; + + emit PayoutReverted(_projectId, _split, _amount, _reason, msg.sender); + } + + // Otherwise, if a project is specified, make a payment to it. + } else if (_split.projectId != 0) { + // Get a reference to the Juicebox terminal being used. + IJBPaymentTerminal _terminal = directory.primaryTerminalOf(_split.projectId, token); + + // The project must have a terminal to send funds to. + if (_terminal == IJBPaymentTerminal(address(0))) { + // Set the net payout amount to 0 to signal the reversion. + netPayoutAmount = 0; + + // Revert the payout. + _revertTransferFrom(_projectId, address(0), 0, _amount); + + emit PayoutReverted(_projectId, _split, _amount, 'Terminal not found', msg.sender); + } else { + // This distribution is eligible for a fee since the funds are leaving this contract and the terminal isn't listed as feeless. + if ( + _terminal != this && + _feeDiscount != JBConstants.MAX_FEE_DISCOUNT && + !isFeelessAddress[address(_terminal)] + ) { + unchecked { + netPayoutAmount -= JBFees.feeIn(_amount, _feePercent, _feeDiscount); + } + } + + // Trigger any inherited pre-transfer logic. + _beforeTransferTo(address(_terminal), netPayoutAmount); + + // Add to balance if prefered. + if (_split.preferAddToBalance) + try + _terminal.addToBalanceOf{value: token == JBTokens.ETH ? netPayoutAmount : 0}( + _split.projectId, + netPayoutAmount, + token, + '', + // Send the projectId in the metadata as a referral. + bytes(abi.encodePacked(_projectId)) + ) + {} catch (bytes memory _reason) { + // Revert the payout. + _revertTransferFrom(_projectId, address(_terminal), netPayoutAmount, _amount); + + // Set the net payout amount to 0 to signal the reversion. + netPayoutAmount = 0; + + emit PayoutReverted(_projectId, _split, _amount, _reason, msg.sender); + } + else + try + _terminal.pay{value: token == JBTokens.ETH ? netPayoutAmount : 0}( + _split.projectId, + netPayoutAmount, + token, + _split.beneficiary != address(0) ? _split.beneficiary : msg.sender, + 0, + _split.preferClaimed, + '', + // Send the projectId in the metadata as a referral. + bytes(abi.encodePacked(_projectId)) + ) + {} catch (bytes memory _reason) { + // Revert the payout. + _revertTransferFrom(_projectId, address(_terminal), netPayoutAmount, _amount); + + // Set the net payout amount to 0 to signal the reversion. + netPayoutAmount = 0; + + emit PayoutReverted(_projectId, _split, _amount, _reason, msg.sender); + } + } + } else { + // This distribution is eligible for a fee since the funds are leaving this contract and the beneficiary isn't listed as feeless. + // Don't enforce feeless address for the beneficiary since the funds are leaving the ecosystem. + if (_feeDiscount != JBConstants.MAX_FEE_DISCOUNT) { + unchecked { + netPayoutAmount -= JBFees.feeIn(_amount, _feePercent, _feeDiscount); + } + } + + // If there's a beneficiary, send the funds directly to the beneficiary. Otherwise send to the msg.sender. + _transferFrom( + address(this), + _split.beneficiary != address(0) ? _split.beneficiary : payable(msg.sender), + netPayoutAmount + ); + } + } + + /// @notice Takes a fee into the platform's project, which has an id of _FEE_BENEFICIARY_PROJECT_ID. + /// @param _projectId The ID of the project having fees taken from. + /// @param _shouldHoldFees If fees should be tracked and held back. + /// @param _feePercent The percent of fees to take, out of MAX_FEE. + /// @param _amount The amount of the fee to take, as a floating point number with 18 decimals. + /// @param _beneficiary The address to mint the platforms tokens for. + /// @param _feeDiscount The amount of discount to apply to the fee, out of the MAX_FEE. + /// @return feeAmount The amount of the fee taken. + function _takeFeeFrom( + uint256 _projectId, + bool _shouldHoldFees, + uint256 _amount, + uint256 _feePercent, + address _beneficiary, + uint256 _feeDiscount + ) internal returns (uint256 feeAmount) { + feeAmount = JBFees.feeIn(_amount, _feePercent, _feeDiscount); + + if (_shouldHoldFees) { + // Store the held fee. + _heldFeesOf[_projectId].push( + JBFee(_amount, uint32(_feePercent), uint32(_feeDiscount), _beneficiary) + ); + + emit HoldFee(_projectId, _amount, _feePercent, _feeDiscount, _beneficiary, msg.sender); + } else { + // Process the fee. + _processFee(feeAmount, _beneficiary, _projectId); // Take the fee. + + emit ProcessFee(_projectId, feeAmount, false, _beneficiary, msg.sender); + } + } + + /// @notice Process a fee of the specified amount. + /// @param _amount The fee amount, as a floating point number with 18 decimals. + /// @param _beneficiary The address to mint the platform's tokens for. + /// @param _from The project ID the fee is being paid from. + function _processFee(uint256 _amount, address _beneficiary, uint256 _from) internal { + // Get the terminal for the protocol project. + IJBPaymentTerminal _terminal = directory.primaryTerminalOf(_FEE_BENEFICIARY_PROJECT_ID, token); + + // Trigger any inherited pre-transfer logic if funds will be transferred. + if (address(_terminal) != address(this)) _beforeTransferTo(address(_terminal), _amount); + + try + // Send the fee. + // If this terminal's token is ETH, send it in msg.value. + _terminal.pay{value: token == JBTokens.ETH ? _amount : 0}( + _FEE_BENEFICIARY_PROJECT_ID, + _amount, + token, + _beneficiary, + 0, + false, + '', + // Send the projectId in the metadata. + bytes(abi.encodePacked(_from)) + ) + {} catch (bytes memory _reason) { + _revertTransferFrom( + _from, + address(_terminal) != address(this) ? address(_terminal) : address(0), + address(_terminal) != address(this) ? _amount : 0, + _amount + ); + emit FeeReverted(_from, _FEE_BENEFICIARY_PROJECT_ID, _amount, _reason, msg.sender); + } + } + + /// @notice Reverts an expected payout. + /// @param _projectId The ID of the project having paying out. + /// @param _expectedDestination The address the payout was expected to go to. + /// @param _allowanceAmount The amount that the destination has been allowed to use. + /// @param _depositAmount The amount of the payout as debited from the project's balance. + function _revertTransferFrom( + uint256 _projectId, + address _expectedDestination, + uint256 _allowanceAmount, + uint256 _depositAmount + ) internal { + // Cancel allowance if needed. + if (_allowanceAmount != 0) _cancelTransferTo(_expectedDestination, _allowanceAmount); + + // Add undistributed amount back to project's balance. + IJBSingleTokenPaymentTerminalStore3_1_1(store).recordAddedBalanceFor( + _projectId, + _depositAmount + ); + } + + /// @notice Contribute tokens to a project. + /// @param _amount The amount of terminal tokens being received, as a fixed point number with the same amount of decimals as this terminal. If this terminal's token is ETH, this is ignored and msg.value is used in its place. + /// @param _payer The address making the payment. + /// @param _projectId The ID of the project being paid. + /// @param _beneficiary The address to mint tokens for and pass along to the funding cycle's data source and delegate. + /// @param _minReturnedTokens The minimum number of project tokens expected in return, as a fixed point number with the same amount of decimals as this terminal. + /// @param _preferClaimedTokens A flag indicating whether the request prefers to mint project tokens into the beneficiaries wallet rather than leaving them unclaimed. This is only possible if the project has an attached token contract. Leaving them unclaimed saves gas. + /// @param _memo A memo to pass along to the emitted event, and passed along the the funding cycle's data source and delegate. A data source can alter the memo before emitting in the event and forwarding to the delegate. + /// @param _metadata Bytes to send along to the data source, delegate, and emitted event, if provided. + /// @return beneficiaryTokenCount The number of tokens minted for the beneficiary, as a fixed point number with 18 decimals. + function _pay( + uint256 _amount, + address _payer, + uint256 _projectId, + address _beneficiary, + uint256 _minReturnedTokens, + bool _preferClaimedTokens, + string memory _memo, + bytes memory _metadata + ) internal returns (uint256 beneficiaryTokenCount) { + // Cant send tokens to the zero address. + if (_beneficiary == address(0)) revert PAY_TO_ZERO_ADDRESS(); + + // Define variables that will be needed outside the scoped section below. + // Keep a reference to the funding cycle during which the payment is being made. + JBFundingCycle memory _fundingCycle; + + // Scoped section prevents stack too deep. `_delegateAllocations` and `_tokenCount` only used within scope. + { + JBPayDelegateAllocation3_1_1[] memory _delegateAllocations; + uint256 _tokenCount; + + // Bundle the amount info into a JBTokenAmount struct. + JBTokenAmount memory _bundledAmount = JBTokenAmount(token, _amount, decimals, currency); + + // Record the payment. + ( + _fundingCycle, + _tokenCount, + _delegateAllocations, + _memo + ) = IJBSingleTokenPaymentTerminalStore3_1_1(store).recordPaymentFrom( + _payer, + _bundledAmount, + _projectId, + _beneficiary, + _memo, + _metadata + ); + + // Mint the tokens if needed. + if (_tokenCount != 0) + // Set token count to be the number of tokens minted for the beneficiary instead of the total amount. + beneficiaryTokenCount = IJBController(directory.controllerOf(_projectId)).mintTokensOf( + _projectId, + _tokenCount, + _beneficiary, + '', + _preferClaimedTokens, + true + ); + + // The token count for the beneficiary must be greater than or equal to the minimum expected. + if (beneficiaryTokenCount < _minReturnedTokens) revert INADEQUATE_TOKEN_COUNT(); + + // If delegate allocations were specified by the data source, fulfill them. + if (_delegateAllocations.length != 0) { + JBDidPayData3_1_1 memory _data = JBDidPayData3_1_1( + _payer, + _projectId, + _fundingCycle.configuration, + _bundledAmount, + JBTokenAmount(token, 0, decimals, currency), + beneficiaryTokenCount, + _beneficiary, + _preferClaimedTokens, + _memo, + bytes(''), + _metadata + ); + + // Get a reference to the number of delegates to allocate to. + uint256 _numDelegates = _delegateAllocations.length; + + // Keep a reference to the allocation. + JBPayDelegateAllocation3_1_1 memory _delegateAllocation; + + for (uint256 _i; _i < _numDelegates; ) { + // Get a reference to the delegate being iterated on. + _delegateAllocation = _delegateAllocations[_i]; + + // Trigger any inherited pre-transfer logic. + _beforeTransferTo(address(_delegateAllocation.delegate), _delegateAllocation.amount); + + // Pass the correct token forwardedAmount to the delegate + _data.forwardedAmount.value = _delegateAllocation.amount; + + // Pass the correct metadata from the data source. + _data.dataSourceMetadata = _delegateAllocation.metadata; + + _delegateAllocation.delegate.didPay{ + value: token == JBTokens.ETH ? _delegateAllocation.amount : 0 + }(_data); + + emit DelegateDidPay( + _delegateAllocation.delegate, + _data, + _delegateAllocation.amount, + msg.sender + ); + + unchecked { + ++_i; + } + } + } + } + + emit Pay( + _fundingCycle.configuration, + _fundingCycle.number, + _projectId, + _payer, + _beneficiary, + _amount, + beneficiaryTokenCount, + _memo, + _metadata, + msg.sender + ); + } + + /// @notice Receives funds belonging to the specified project. + /// @param _projectId The ID of the project to which the funds received belong. + /// @param _amount The amount of tokens to add, as a fixed point number with the same number of decimals as this terminal. If this is an ETH terminal, this is ignored and msg.value is used instead. + /// @param _shouldRefundHeldFees A flag indicating if held fees should be refunded based on the amount being added. + /// @param _memo A memo to pass along to the emitted event. + /// @param _metadata Extra data to pass along to the emitted event. + function _addToBalanceOf( + uint256 _projectId, + uint256 _amount, + bool _shouldRefundHeldFees, + string memory _memo, + bytes memory _metadata + ) internal { + // Refund any held fees to make sure the project doesn't pay double for funds going in and out of the protocol. + uint256 _refundedFees = _shouldRefundHeldFees ? _refundHeldFees(_projectId, _amount) : 0; + + // Record the added funds with any refunded fees. + IJBSingleTokenPaymentTerminalStore3_1_1(store).recordAddedBalanceFor( + _projectId, + _amount + _refundedFees + ); + + emit AddToBalance(_projectId, _amount, _refundedFees, _memo, _metadata, msg.sender); + } + + /// @notice Refund fees based on the specified amount. + /// @param _projectId The project for which fees are being refunded. + /// @param _amount The amount to base the refund on, as a fixed point number with the same amount of decimals as this terminal. + /// @return refundedFees How much fees were refunded, as a fixed point number with the same number of decimals as this terminal + function _refundHeldFees( + uint256 _projectId, + uint256 _amount + ) internal returns (uint256 refundedFees) { + // Get a reference to the project's held fees. + JBFee[] memory _heldFees = _heldFeesOf[_projectId]; + + // Delete the current held fees. + delete _heldFeesOf[_projectId]; + + // Get a reference to the leftover amount once all fees have been settled. + uint256 leftoverAmount = _amount; + + // Push length in stack + uint256 _heldFeesLength = _heldFees.length; + + // Process each fee. + for (uint256 _i; _i < _heldFeesLength; ) { + if (leftoverAmount == 0) { + _heldFeesOf[_projectId].push(_heldFees[_i]); + } else { + // Notice here we take feeIn the stored .amount + uint256 _feeAmount = ( + _heldFees[_i].fee == 0 || _heldFees[_i].feeDiscount == JBConstants.MAX_FEE_DISCOUNT + ? 0 + : JBFees.feeIn(_heldFees[_i].amount, _heldFees[_i].fee, _heldFees[_i].feeDiscount) + ); + + if (leftoverAmount >= _heldFees[_i].amount - _feeAmount) { + unchecked { + leftoverAmount = leftoverAmount - (_heldFees[_i].amount - _feeAmount); + refundedFees += _feeAmount; + } + } else { + // And here we overwrite with feeFrom the leftoverAmount + _feeAmount = ( + _heldFees[_i].fee == 0 || _heldFees[_i].feeDiscount == JBConstants.MAX_FEE_DISCOUNT + ? 0 + : JBFees.feeFrom(leftoverAmount, _heldFees[_i].fee, _heldFees[_i].feeDiscount) + ); + + unchecked { + _heldFeesOf[_projectId].push( + JBFee( + _heldFees[_i].amount - (leftoverAmount + _feeAmount), + _heldFees[_i].fee, + _heldFees[_i].feeDiscount, + _heldFees[_i].beneficiary + ) + ); + refundedFees += _feeAmount; + } + leftoverAmount = 0; + } + } + + unchecked { + ++_i; + } + } + + emit RefundHeldFees(_projectId, _amount, refundedFees, leftoverAmount, msg.sender); + } + + /// @notice Get the fee discount from the fee gauge for the specified project. + /// @param _projectId The ID of the project to get a fee discount for. + /// @param _feeType The type of fee the discount is being applied to. + /// @return feeDiscount The fee discount, which should be interpreted as a percentage out MAX_FEE_DISCOUNT. + function _currentFeeDiscount( + uint256 _projectId, + JBFeeType _feeType + ) internal view returns (uint256) { + // Can't take a fee if the protocol project doesn't have a terminal that accepts the token. + if ( + directory.primaryTerminalOf(_FEE_BENEFICIARY_PROJECT_ID, token) == + IJBPaymentTerminal(address(0)) + ) return JBConstants.MAX_FEE_DISCOUNT; + + // Get the fee discount. + if (feeGauge != address(0)) + // If the guage reverts, keep the discount at 0. + try IJBFeeGauge3_1(feeGauge).currentDiscountFor(_projectId, _feeType) returns ( + uint256 discount + ) { + // If the fee discount is greater than the max, we ignore the return value + if (discount <= JBConstants.MAX_FEE_DISCOUNT) return discount; + } catch { + return 0; + } + + return 0; + } +} diff --git a/contracts/interfaces/IJBController3_2.sol b/contracts/interfaces/IJBController3_2.sol new file mode 100644 index 000000000..6f03e523f --- /dev/null +++ b/contracts/interfaces/IJBController3_2.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC165} from '@openzeppelin/contracts/utils/introspection/IERC165.sol'; +import {JBBallotState} from './../enums/JBBallotState.sol'; +import {JBFundAccessConstraints} from './../structs/JBFundAccessConstraints.sol'; +import {JBFundingCycle} from './../structs/JBFundingCycle.sol'; +import {JBFundingCycleData} from './../structs/JBFundingCycleData.sol'; +import {JBFundingCycleMetadata3_2} from './../structs/JBFundingCycleMetadata3_2.sol'; +import {JBGroupedSplits} from './../structs/JBGroupedSplits.sol'; +import {JBProjectMetadata} from './../structs/JBProjectMetadata.sol'; +import {JBSplit} from './../structs/JBSplit.sol'; +import {IJBController3_0_1} from './IJBController3_0_1.sol'; +import {IJBDirectory} from './IJBDirectory.sol'; +import {IJBFundAccessConstraintsStore} from './IJBFundAccessConstraintsStore.sol'; +import {IJBFundingCycleStore} from './IJBFundingCycleStore.sol'; +import {IJBMigratable} from './IJBMigratable.sol'; +import {IJBPaymentTerminal} from './IJBPaymentTerminal.sol'; +import {IJBProjects} from './IJBProjects.sol'; +import {IJBSplitsStore} from './IJBSplitsStore.sol'; +import {IJBTokenStore} from './IJBTokenStore.sol'; + +interface IJBController3_2 is IJBController3_0_1, IERC165 { + event LaunchProject(uint256 configuration, uint256 projectId, string memo, address caller); + + event LaunchFundingCycles(uint256 configuration, uint256 projectId, string memo, address caller); + + event ReconfigureFundingCycles( + uint256 configuration, + uint256 projectId, + string memo, + address caller + ); + + event DistributeReservedTokens( + uint256 indexed fundingCycleConfiguration, + uint256 indexed fundingCycleNumber, + uint256 indexed projectId, + address beneficiary, + uint256 tokenCount, + uint256 beneficiaryTokenCount, + string memo, + address caller + ); + + event DistributeToReservedTokenSplit( + uint256 indexed projectId, + uint256 indexed domain, + uint256 indexed group, + JBSplit split, + uint256 tokenCount, + address caller + ); + + event MintTokens( + address indexed beneficiary, + uint256 indexed projectId, + uint256 tokenCount, + uint256 beneficiaryTokenCount, + string memo, + uint256 reservedRate, + address caller + ); + + event BurnTokens( + address indexed holder, + uint256 indexed projectId, + uint256 tokenCount, + string memo, + address caller + ); + + event Migrate(uint256 indexed projectId, IJBMigratable to, address caller); + + event PrepMigration(uint256 indexed projectId, address from, address caller); + + function projects() external view returns (IJBProjects); + + function fundingCycleStore() external view returns (IJBFundingCycleStore); + + function tokenStore() external view returns (IJBTokenStore); + + function splitsStore() external view returns (IJBSplitsStore); + + function fundAccessConstraintsStore() external view returns (IJBFundAccessConstraintsStore); + + function directory() external view returns (IJBDirectory); + + function reservedTokenBalanceOf(uint256 projectId) external view returns (uint256); + + function totalOutstandingTokensOf(uint256 projectId) external view returns (uint256); + + function getFundingCycleOf( + uint256 projectId, + uint256 configuration + ) + external + view + returns (JBFundingCycle memory fundingCycle, JBFundingCycleMetadata3_2 memory metadata); + + function latestConfiguredFundingCycleOf( + uint256 projectId + ) + external + view + returns (JBFundingCycle memory, JBFundingCycleMetadata3_2 memory metadata, JBBallotState); + + function currentFundingCycleOf( + uint256 projectId + ) + external + view + returns (JBFundingCycle memory fundingCycle, JBFundingCycleMetadata3_2 memory metadata); + + function queuedFundingCycleOf( + uint256 projectId + ) + external + view + returns (JBFundingCycle memory fundingCycle, JBFundingCycleMetadata3_2 memory metadata); + + function launchProjectFor( + address owner, + JBProjectMetadata calldata projectMetadata, + JBFundingCycleData calldata data, + JBFundingCycleMetadata3_2 calldata metadata, + uint256 mustStartAtOrAfter, + JBGroupedSplits[] memory groupedSplits, + JBFundAccessConstraints[] memory fundAccessConstraints, + IJBPaymentTerminal[] memory terminals, + string calldata memo + ) external returns (uint256 projectId); + + function launchFundingCyclesFor( + uint256 projectId, + JBFundingCycleData calldata data, + JBFundingCycleMetadata3_2 calldata metadata, + uint256 mustStartAtOrAfter, + JBGroupedSplits[] memory groupedSplits, + JBFundAccessConstraints[] memory fundAccessConstraints, + IJBPaymentTerminal[] memory terminals, + string calldata memo + ) external returns (uint256 configuration); + + function reconfigureFundingCyclesOf( + uint256 projectId, + JBFundingCycleData calldata data, + JBFundingCycleMetadata3_2 calldata metadata, + uint256 mustStartAtOrAfter, + JBGroupedSplits[] memory groupedSplits, + JBFundAccessConstraints[] memory fundAccessConstraints, + string calldata memo + ) external returns (uint256); + + function mintTokensOf( + uint256 projectId, + uint256 tokenCount, + address beneficiary, + string calldata memo, + bool preferClaimedTokens, + bool useReservedRate + ) external returns (uint256 beneficiaryTokenCount); + + function burnTokensOf( + address holder, + uint256 projectId, + uint256 tokenCount, + string calldata memo, + bool preferClaimedTokens + ) external; + + function distributeReservedTokensOf( + uint256 projectId, + string memory memo + ) external returns (uint256); + + function migrate(uint256 projectId, IJBMigratable to) external; +} diff --git a/contracts/interfaces/IJBPayoutRedemptionPaymentTerminal3_2.sol b/contracts/interfaces/IJBPayoutRedemptionPaymentTerminal3_2.sol new file mode 100644 index 000000000..248091340 --- /dev/null +++ b/contracts/interfaces/IJBPayoutRedemptionPaymentTerminal3_2.sol @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {JBFee} from './../structs/JBFee.sol'; +import {IJBAllowanceTerminal3_1} from './IJBAllowanceTerminal3_1.sol'; +import {IJBDirectory} from './IJBDirectory.sol'; +import {IJBFeeHoldingTerminal} from './IJBFeeHoldingTerminal.sol'; +import {IJBPayDelegate3_1_1} from './IJBPayDelegate3_1_1.sol'; +import {IJBPaymentTerminal} from './IJBPaymentTerminal.sol'; +import {IJBPayoutTerminal3_1} from './IJBPayoutTerminal3_1.sol'; +import {IJBPrices} from './IJBPrices.sol'; +import {IJBProjects} from './IJBProjects.sol'; +import {IJBRedemptionDelegate3_1_1} from './IJBRedemptionDelegate3_1_1.sol'; +import {IJBRedemptionTerminal} from './IJBRedemptionTerminal.sol'; +import {IJBSplitsStore} from './IJBSplitsStore.sol'; +import {JBDidPayData3_1_1} from './../structs/JBDidPayData3_1_1.sol'; +import {JBDidRedeemData3_1_1} from './../structs/JBDidRedeemData3_1_1.sol'; +import {JBSplit} from './../structs/JBSplit.sol'; + +interface IJBPayoutRedemptionPaymentTerminal3_2 is + IJBPaymentTerminal, + IJBPayoutTerminal3_1, + IJBAllowanceTerminal3_1, + IJBRedemptionTerminal, + IJBFeeHoldingTerminal +{ + event AddToBalance( + uint256 indexed projectId, + uint256 amount, + uint256 refundedFees, + string memo, + bytes metadata, + address caller + ); + + event Migrate( + uint256 indexed projectId, + IJBPaymentTerminal indexed to, + uint256 amount, + address caller + ); + + event DistributePayouts( + uint256 indexed fundingCycleConfiguration, + uint256 indexed fundingCycleNumber, + uint256 indexed projectId, + address beneficiary, + uint256 amount, + uint256 distributedAmount, + uint256 fee, + uint256 beneficiaryDistributionAmount, + bytes metadata, + address caller + ); + + event UseAllowance( + uint256 indexed fundingCycleConfiguration, + uint256 indexed fundingCycleNumber, + uint256 indexed projectId, + address beneficiary, + uint256 amount, + uint256 distributedAmount, + uint256 netDistributedamount, + string memo, + bytes metadata, + address caller + ); + + event HoldFee( + uint256 indexed projectId, + uint256 indexed amount, + uint256 indexed fee, + uint256 feeDiscount, + address beneficiary, + address caller + ); + + event ProcessFee( + uint256 indexed projectId, + uint256 indexed amount, + bool indexed wasHeld, + address beneficiary, + address caller + ); + + event RefundHeldFees( + uint256 indexed projectId, + uint256 indexed amount, + uint256 indexed refundedFees, + uint256 leftoverAmount, + address caller + ); + + event Pay( + uint256 indexed fundingCycleConfiguration, + uint256 indexed fundingCycleNumber, + uint256 indexed projectId, + address payer, + address beneficiary, + uint256 amount, + uint256 beneficiaryTokenCount, + string memo, + bytes metadata, + address caller + ); + + event RedeemTokens( + uint256 indexed fundingCycleConfiguration, + uint256 indexed fundingCycleNumber, + uint256 indexed projectId, + address holder, + address beneficiary, + uint256 tokenCount, + uint256 reclaimedAmount, + string memo, + bytes metadata, + address caller + ); + + event DistributeToPayoutSplit( + uint256 indexed projectId, + uint256 indexed domain, + uint256 indexed group, + JBSplit split, + uint256 amount, + uint256 netAmount, + address caller + ); + + event SetFee(uint256 fee, address caller); + + event SetFeeGauge(address indexed feeGauge, address caller); + + event SetFeelessAddress(address indexed addrs, bool indexed flag, address caller); + + event PayoutReverted( + uint256 indexed projectId, + JBSplit split, + uint256 amount, + bytes reason, + address caller + ); + + event FeeReverted( + uint256 indexed projectId, + uint256 indexed feeProjectId, + uint256 amount, + bytes reason, + address caller + ); + + event DelegateDidRedeem( + IJBRedemptionDelegate3_1_1 indexed delegate, + JBDidRedeemData3_1_1 data, + uint256 delegatedAmount, + uint256 fee, + address caller + ); + + event DelegateDidPay( + IJBPayDelegate3_1_1 indexed delegate, + JBDidPayData3_1_1 data, + uint256 delegatedAmount, + address caller + ); + + function projects() external view returns (IJBProjects); + + function splitsStore() external view returns (IJBSplitsStore); + + function directory() external view returns (IJBDirectory); + + function prices() external view returns (IJBPrices); + + function store() external view returns (address); + + function payoutSplitsGroup() external view returns (uint256); + + function heldFeesOf(uint256 projectId) external view returns (JBFee[] memory); + + function fee() external view returns (uint256); + + function feeGauge() external view returns (address); + + function isFeelessAddress(address account) external view returns (bool); + + function migrate(uint256 projectId, IJBPaymentTerminal to) external returns (uint256 balance); + + function processFees(uint256 projectId) external; + + function setFee(uint256 fee) external; + + function setFeeGauge(address feeGauge) external; + + function setFeelessAddress(address account, bool flag) external; +} diff --git a/contracts/interfaces/IJBSingleTokenPaymentTerminalStore3_2.sol b/contracts/interfaces/IJBSingleTokenPaymentTerminalStore3_2.sol new file mode 100644 index 000000000..df2db4f6d --- /dev/null +++ b/contracts/interfaces/IJBSingleTokenPaymentTerminalStore3_2.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {JBFundingCycle} from './../structs/JBFundingCycle.sol'; +import {JBPayDelegateAllocation3_1_1} from './../structs/JBPayDelegateAllocation3_1_1.sol'; +import {JBRedemptionDelegateAllocation3_1_1} from './../structs/JBRedemptionDelegateAllocation3_1_1.sol'; +import {JBTokenAmount} from './../structs/JBTokenAmount.sol'; +import {IJBDirectory} from './IJBDirectory.sol'; +import {IJBFundingCycleStore} from './IJBFundingCycleStore.sol'; +import {IJBPrices} from './IJBPrices.sol'; +import {IJBSingleTokenPaymentTerminal} from './IJBSingleTokenPaymentTerminal.sol'; + +interface IJBSingleTokenPaymentTerminalStore3_2 { + function fundingCycleStore() external view returns (IJBFundingCycleStore); + + function directory() external view returns (IJBDirectory); + + function prices() external view returns (IJBPrices); + + function balanceOf( + IJBSingleTokenPaymentTerminal terminal, + uint256 projectId + ) external view returns (uint256); + + function usedDistributionLimitOf( + IJBSingleTokenPaymentTerminal terminal, + uint256 projectId, + uint256 fundingCycleNumber + ) external view returns (uint256); + + function usedOverflowAllowanceOf( + IJBSingleTokenPaymentTerminal terminal, + uint256 projectId, + uint256 fundingCycleConfiguration + ) external view returns (uint256); + + function currentOverflowOf( + IJBSingleTokenPaymentTerminal terminal, + uint256 projectId + ) external view returns (uint256); + + function currentTotalOverflowOf( + uint256 projectId, + uint256 decimals, + uint256 currency + ) external view returns (uint256); + + function currentReclaimableOverflowOf( + IJBSingleTokenPaymentTerminal terminal, + uint256 projectId, + uint256 tokenCount, + bool useTotalOverflow + ) external view returns (uint256); + + function currentReclaimableOverflowOf( + uint256 projectId, + uint256 tokenCount, + uint256 totalSupply, + uint256 overflow + ) external view returns (uint256); + + function recordPaymentFrom( + address payer, + JBTokenAmount memory amount, + uint256 projectId, + address beneficiary, + string calldata inputMemo, + bytes calldata metadata + ) + external + returns ( + JBFundingCycle memory fundingCycle, + uint256 tokenCount, + JBPayDelegateAllocation3_1_1[] memory delegateAllocations, + string memory outputMemo + ); + + function recordRedemptionFor( + address holder, + uint256 projectId, + uint256 tokenCount, + string calldata inputMemo, + bytes calldata metadata + ) + external + returns ( + JBFundingCycle memory fundingCycle, + uint256 reclaimAmount, + JBRedemptionDelegateAllocation3_1_1[] memory delegateAllocations, + string memory outputMemo + ); + + function recordDistributionFor( + uint256 projectId, + uint256 amount, + uint256 currency + ) external returns (JBFundingCycle memory fundingCycle, uint256 distributedAmount); + + function recordUsedAllowanceOf( + uint256 projectId, + uint256 amount, + uint256 currency + ) external returns (JBFundingCycle memory fundingCycle, uint256 withdrawnAmount); + + function recordAddedBalanceFor(uint256 projectId, uint256 amount) external; + + function recordMigration(uint256 projectId) external returns (uint256 balance); +} diff --git a/contracts/libraries/JBFundingCycleMetadataResolver3_2.sol b/contracts/libraries/JBFundingCycleMetadataResolver3_2.sol new file mode 100644 index 000000000..0fa00549b --- /dev/null +++ b/contracts/libraries/JBFundingCycleMetadataResolver3_2.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import {JBFundingCycle} from './../structs/JBFundingCycle.sol'; +import {JBFundingCycleMetadata3_2} from './../structs/JBFundingCycleMetadata3_2.sol'; +import {JBGlobalFundingCycleMetadata} from './../structs/JBGlobalFundingCycleMetadata.sol'; +import {JBConstants} from './JBConstants.sol'; +import {JBGlobalFundingCycleMetadataResolver} from './JBGlobalFundingCycleMetadataResolver.sol'; + +library JBFundingCycleMetadataResolver3_2 { + function global(JBFundingCycle memory _fundingCycle) + internal + pure + returns (JBGlobalFundingCycleMetadata memory) + { + return JBGlobalFundingCycleMetadataResolver.expandMetadata(uint8(_fundingCycle.metadata >> 8)); + } + + function reservedRate(JBFundingCycle memory _fundingCycle) internal pure returns (uint256) { + return uint256(uint16(_fundingCycle.metadata >> 16)); + } + + function redemptionRate(JBFundingCycle memory _fundingCycle) internal pure returns (uint256) { + // Redemption rate is a number 0-10000. + return uint256(uint16(_fundingCycle.metadata >> 32)); + } + + function baseCurrency(JBFundingCycle memory _fundingCycle) + internal + pure + returns (uint256) + { + // Currency is a number 0-16777215. + return uint256(uint24(_fundingCycle.metadata >> 48)); + } + + function payPaused(JBFundingCycle memory _fundingCycle) internal pure returns (bool) { + return ((_fundingCycle.metadata >> 72) & 1) == 1; + } + + function distributionsPaused(JBFundingCycle memory _fundingCycle) internal pure returns (bool) { + return ((_fundingCycle.metadata >> 73) & 1) == 1; + } + + function redeemPaused(JBFundingCycle memory _fundingCycle) internal pure returns (bool) { + return ((_fundingCycle.metadata >> 74) & 1) == 1; + } + + function burnPaused(JBFundingCycle memory _fundingCycle) internal pure returns (bool) { + return ((_fundingCycle.metadata >> 75) & 1) == 1; + } + + function mintingAllowed(JBFundingCycle memory _fundingCycle) internal pure returns (bool) { + return ((_fundingCycle.metadata >> 76) & 1) == 1; + } + + function terminalMigrationAllowed(JBFundingCycle memory _fundingCycle) + internal + pure + returns (bool) + { + return ((_fundingCycle.metadata >> 77) & 1) == 1; + } + + function controllerMigrationAllowed(JBFundingCycle memory _fundingCycle) + internal + pure + returns (bool) + { + return ((_fundingCycle.metadata >> 78) & 1) == 1; + } + + function shouldHoldFees(JBFundingCycle memory _fundingCycle) internal pure returns (bool) { + return ((_fundingCycle.metadata >> 79) & 1) == 1; + } + + function preferClaimedTokenOverride(JBFundingCycle memory _fundingCycle) + internal + pure + returns (bool) + { + return ((_fundingCycle.metadata >> 80) & 1) == 1; + } + + function useTotalOverflowForRedemptions(JBFundingCycle memory _fundingCycle) + internal + pure + returns (bool) + { + return ((_fundingCycle.metadata >> 81) & 1) == 1; + } + + function useDataSourceForPay(JBFundingCycle memory _fundingCycle) internal pure returns (bool) { + return (_fundingCycle.metadata >> 82) & 1 == 1; + } + + function useDataSourceForRedeem(JBFundingCycle memory _fundingCycle) + internal + pure + returns (bool) + { + return (_fundingCycle.metadata >> 83) & 1 == 1; + } + + function dataSource(JBFundingCycle memory _fundingCycle) internal pure returns (address) { + return address(uint160(_fundingCycle.metadata >> 84)); + } + + function metadata(JBFundingCycle memory _fundingCycle) internal pure returns (uint256) { + return uint256(uint8(_fundingCycle.metadata >> 244)); + } + + /// @notice Pack the funding cycle metadata. + /// @param _metadata The metadata to validate and pack. + /// @return packed The packed uint256 of all metadata params. The first 8 bits specify the version. + function packFundingCycleMetadata(JBFundingCycleMetadata3_2 memory _metadata) + internal + pure + returns (uint256 packed) + { + // version 2 in the bits 0-7 (8 bits). + packed = 2; + // global metadata in bits 8-15 (8 bits). + packed |= + JBGlobalFundingCycleMetadataResolver.packFundingCycleGlobalMetadata(_metadata.global) << + 8; + // reserved rate in bits 16-31 (16 bits). + packed |= _metadata.reservedRate << 16; + // redemption rate in bits 32-47 (16 bits). + // redemption rate is a number 0-10000. + packed |= _metadata.redemptionRate << 32; + // base currency in bits 48-71 (24 bits). + // base currency is a number 0-16777215. + packed |= _metadata.baseCurrency << 56; + // pause pay in bit 72. + if (_metadata.pausePay) packed |= 1 << 72; + // pause tap in bit 73. + if (_metadata.pauseDistributions) packed |= 1 << 73; + // pause redeem in bit 74. + if (_metadata.pauseRedeem) packed |= 1 << 74; + // pause burn in bit 75. + if (_metadata.pauseBurn) packed |= 1 << 75; + // allow minting in bit 76. + if (_metadata.allowMinting) packed |= 1 << 76; + // allow terminal migration in bit 77. + if (_metadata.allowTerminalMigration) packed |= 1 << 77; + // allow controller migration in bit 78. + if (_metadata.allowControllerMigration) packed |= 1 << 78; + // hold fees in bit 79. + if (_metadata.holdFees) packed |= 1 << 79; + // prefer claimed token override in bit 80. + if (_metadata.preferClaimedTokenOverride) packed |= 1 << 80; + // useTotalOverflowForRedemptions in bit 81. + if (_metadata.useTotalOverflowForRedemptions) packed |= 1 << 81; + // use pay data source in bit 82. + if (_metadata.useDataSourceForPay) packed |= 1 << 82; + // use redeem data source in bit 83. + if (_metadata.useDataSourceForRedeem) packed |= 1 << 83; + // data source address in bits 84-243. + packed |= uint256(uint160(address(_metadata.dataSource))) << 84; + // metadata in bits 244-252 (8 bits). + packed |= _metadata.metadata << 244; + } + + /// @notice Expand the funding cycle metadata. + /// @param _fundingCycle The funding cycle having its metadata expanded. + /// @return metadata The metadata object. + function expandMetadata(JBFundingCycle memory _fundingCycle) + internal + pure + returns (JBFundingCycleMetadata3_2 memory) + { + return + JBFundingCycleMetadata3_2( + global(_fundingCycle), + reservedRate(_fundingCycle), + redemptionRate(_fundingCycle), + baseCurrency(_fundingCycle), + payPaused(_fundingCycle), + distributionsPaused(_fundingCycle), + redeemPaused(_fundingCycle), + burnPaused(_fundingCycle), + mintingAllowed(_fundingCycle), + terminalMigrationAllowed(_fundingCycle), + controllerMigrationAllowed(_fundingCycle), + shouldHoldFees(_fundingCycle), + preferClaimedTokenOverride(_fundingCycle), + useTotalOverflowForRedemptions(_fundingCycle), + useDataSourceForPay(_fundingCycle), + useDataSourceForRedeem(_fundingCycle), + dataSource(_fundingCycle), + metadata(_fundingCycle) + ); + } +} diff --git a/contracts/structs/JBFundingCycleMetadata3_2.sol b/contracts/structs/JBFundingCycleMetadata3_2.sol new file mode 100644 index 000000000..0e444cd3d --- /dev/null +++ b/contracts/structs/JBFundingCycleMetadata3_2.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {JBGlobalFundingCycleMetadata} from './JBGlobalFundingCycleMetadata.sol'; + +/// @custom:member global Data used globally in non-migratable ecosystem contracts. +/// @custom:member reservedRate The reserved rate of the funding cycle. This number is a percentage calculated out of `JBConstants.MAX_RESERVED_RATE`. +/// @custom:member redemptionRate The redemption rate of the funding cycle. This number is a percentage calculated out of `JBConstants.MAX_REDEMPTION_RATE`. +/// @custom:member baseCurrency The currency on which to base the funding cycle's weight. +/// @custom:member pausePay A flag indicating if the pay functionality should be paused during the funding cycle. +/// @custom:member pauseDistributions A flag indicating if the distribute functionality should be paused during the funding cycle. +/// @custom:member pauseRedeem A flag indicating if the redeem functionality should be paused during the funding cycle. +/// @custom:member pauseBurn A flag indicating if the burn functionality should be paused during the funding cycle. +/// @custom:member allowMinting A flag indicating if minting tokens should be allowed during this funding cycle. +/// @custom:member allowTerminalMigration A flag indicating if migrating terminals should be allowed during this funding cycle. +/// @custom:member allowControllerMigration A flag indicating if migrating controllers should be allowed during this funding cycle. +/// @custom:member holdFees A flag indicating if fees should be held during this funding cycle. +/// @custom:member preferClaimedTokenOverride A flag indicating if claimed tokens should always be prefered to unclaimed tokens when minting. +/// @custom:member useTotalOverflowForRedemptions 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. +/// @custom:member useDataSourceForPay A flag indicating if the data source should be used for pay transactions during this funding cycle. +/// @custom:member useDataSourceForRedeem A flag indicating if the data source should be used for redeem transactions during this funding cycle. +/// @custom:member dataSource The data source to use during this funding cycle. +/// @custom:member metadata Metadata of the metadata, up to uint8 in size. +struct JBFundingCycleMetadata3_2 { + JBGlobalFundingCycleMetadata global; + uint256 reservedRate; + uint256 redemptionRate; + uint256 baseCurrency; + bool pausePay; + bool pauseDistributions; + bool pauseRedeem; + bool pauseBurn; + bool allowMinting; + bool allowTerminalMigration; + bool allowControllerMigration; + bool holdFees; + bool preferClaimedTokenOverride; + bool useTotalOverflowForRedemptions; + bool useDataSourceForPay; + bool useDataSourceForRedeem; + address dataSource; + uint256 metadata; +}