diff --git a/contracts/JBPrices3_2.sol b/contracts/JBPrices3_2.sol new file mode 100644 index 000000000..8624d70d3 --- /dev/null +++ b/contracts/JBPrices3_2.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; +import {JBOperatable} from './abstract/JBOperatable.sol'; +import {PRBMath} from '@paulrberg/contracts/math/PRBMath.sol'; +import {IJBPriceFeed} from './interfaces/IJBPriceFeed.sol'; +import {IJBProjects} from './interfaces/IJBProjects.sol'; +import {IJBOperatorStore} from './interfaces/IJBOperatorStore.sol'; +import {IJBPrices3_2} from './interfaces/IJBPrices3_2.sol'; +import {JBOperations2} from './libraries/JBOperations2.sol'; + +/// @notice Manages and normalizes price feeds. +contract JBPrices3_2 is Ownable, JBOperatable, IJBPrices3_2 { + //*********************************************************************// + // --------------------------- custom errors ------------------------- // + //*********************************************************************// + error PRICE_FEED_ALREADY_EXISTS(); + error PRICE_FEED_NOT_FOUND(); + + //*********************************************************************// + // --------------------- internal stored constants ------------------- // + //*********************************************************************// + + /// @notice The ID to store default values in. + uint256 public constant override DEFAULT_PROJECT_ID = 0; + + //*********************************************************************// + // ---------------- public immutable stored properties --------------- // + //*********************************************************************// + + /// @notice Mints ERC-721's that represent project ownership and transfers. + IJBProjects public immutable override projects; + + //*********************************************************************// + // --------------------- public stored properties -------------------- // + //*********************************************************************// + + /// @notice The available price feeds. + /// @dev The feed returns the number of `_currency` units that can be converted to 1 `_base` unit. + /// @custom:param _projectId The ID of the project for which the feed applies. Feeds stored in ID 0 are used by default. + /// @custom:param _currency The currency units the feed's resulting price is in terms of. + /// @custom:param _base The base currency unit being priced by the feed. + mapping(uint256 => mapping(uint256 => mapping(uint256 => IJBPriceFeed))) public override feedFor; + + //*********************************************************************// + // -------------------------- public views --------------------------- // + //*********************************************************************// + + /// @notice Gets the number of `_currency` units that can be converted to 1 `_base` unit. + /// @param _projectId The ID of the project relative to which the feed used to derive the price belongs. Feeds stored in ID 0 are used by default. + /// @param _currency The currency units the resulting price is in terms of. + /// @param _base The base currency unit being priced. + /// @param _decimals The number of decimals the returned fixed point price should include. + /// @return The price of the currency in terms of the base, as a fixed point number with the specified number of decimals. + function priceFor( + uint256 _projectId, + uint256 _currency, + uint256 _base, + uint256 _decimals + ) public view override returns (uint256) { + // If the currency is the base, return 1 since they are priced the same. Include the desired number of decimals. + if (_currency == _base) return 10 ** _decimals; + + // Get a reference to the feed. + IJBPriceFeed _feed = feedFor[_projectId][_currency][_base]; + + // If it exists, return the price. + if (_feed != IJBPriceFeed(address(0))) return _feed.currentPrice(_decimals); + + // Get the inverse feed. + _feed = feedFor[_projectId][_base][_currency]; + + // If it exists, return the inverse price. + if (_feed != IJBPriceFeed(address(0))) + return PRBMath.mulDiv(10 ** _decimals, 10 ** _decimals, _feed.currentPrice(_decimals)); + + // Check in the 0 project if not found. + if (_projectId != 0) + return priceFor({_projectId: 0, _currency: _currency, _base: _base, _decimals: _decimals}); + + // No price feed available, revert. + revert PRICE_FEED_NOT_FOUND(); + } + + //*********************************************************************// + // ---------------------------- constructor -------------------------- // + //*********************************************************************// + + /// @param _operatorStore A contract storing operator assignments. + /// @param _projects A contract which mints ERC-721's that represent project ownership and transfers. + /// @param _owner The address that will own the contract. + constructor( + IJBOperatorStore _operatorStore, + IJBProjects _projects, + address _owner + ) JBOperatable(_operatorStore) { + projects = _projects; + // Transfer the ownership. + transferOwnership(_owner); + } + + //*********************************************************************// + // ---------------------- external transactions ---------------------- // + //*********************************************************************// + + /// @notice Add a price feed for a currency in terms of the provided base currency. + /// @dev Current feeds can't be modified, neither can feeds that have already been set by the default. + /// @param _currency The currency units the feed's resulting price is in terms of. + /// @param _base The base currency unit being priced by the feed. + /// @param _feed The price feed being added. + function addFeedFor( + uint256 _projectId, + uint256 _currency, + uint256 _base, + IJBPriceFeed _feed + ) + external + override + requirePermissionAllowingOverride( + projects.ownerOf(_projectId), + _projectId, + JBOperations2.ADD_PRICE_FEED, + msg.sender == owner() && _projectId == 0 + ) + { + // Make sure there's no feed stored for the pair as defaults. + if ( + feedFor[0][_currency][_base] != IJBPriceFeed(address(0)) || + feedFor[0][_base][_currency] != IJBPriceFeed(address(0)) + ) { + revert PRICE_FEED_ALREADY_EXISTS(); + } + + // There can't already be a feed for the specified currency. + if ( + feedFor[_projectId][_currency][_base] != IJBPriceFeed(address(0)) || + feedFor[_projectId][_base][_currency] != IJBPriceFeed(address(0)) + ) revert PRICE_FEED_ALREADY_EXISTS(); + + // Store the feed. + feedFor[_projectId][_currency][_base] = _feed; + + emit AddFeed(_projectId, _currency, _base, _feed); + } +} diff --git a/contracts/JBSingleTokenPaymentTerminalStore3_2.sol b/contracts/JBSingleTokenPaymentTerminalStore3_2.sol index e60a6cc86..3f4b8e73e 100644 --- a/contracts/JBSingleTokenPaymentTerminalStore3_2.sol +++ b/contracts/JBSingleTokenPaymentTerminalStore3_2.sol @@ -9,7 +9,7 @@ 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 {IJBPrices3_2} from './interfaces/IJBPrices3_2.sol'; import {IJBSingleTokenPaymentTerminal} from './interfaces/IJBSingleTokenPaymentTerminal.sol'; import {IJBSingleTokenPaymentTerminalStore3_2} from './interfaces/IJBSingleTokenPaymentTerminalStore3_2.sol'; import {JBConstants} from './libraries/JBConstants.sol'; @@ -65,7 +65,7 @@ contract JBSingleTokenPaymentTerminalStore3_1_1 is IJBFundingCycleStore public immutable override fundingCycleStore; /// @notice The contract that exposes price feeds. - IJBPrices public immutable override prices; + IJBPrices3_2 public immutable override prices; //*********************************************************************// // --------------------- public stored properties -------------------- // @@ -212,7 +212,11 @@ contract JBSingleTokenPaymentTerminalStore3_1_1 is /// @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) { + constructor( + IJBDirectory _directory, + IJBFundingCycleStore _fundingCycleStore, + IJBPrices3_2 _prices + ) { directory = _directory; fundingCycleStore = _fundingCycleStore; prices = _prices; @@ -336,7 +340,7 @@ contract JBSingleTokenPaymentTerminalStore3_1_1 is // 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); + : prices.priceFor(_projectId, _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); @@ -545,7 +549,7 @@ contract JBSingleTokenPaymentTerminalStore3_1_1 is : 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) + prices.priceFor(_projectId, _currency, _balanceCurrency, _MAX_FIXED_POINT_FIDELITY) ); // The amount being distributed must be available. @@ -616,7 +620,7 @@ contract JBSingleTokenPaymentTerminalStore3_1_1 is : 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) + prices.priceFor(_projectId, _currency, _balanceCurrency, _MAX_FIXED_POINT_FIDELITY) ); // The amount being distributed must be available in the overflow. @@ -757,7 +761,12 @@ contract JBSingleTokenPaymentTerminalStore3_1_1 is _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) + prices.priceFor( + _projectId, + _distributionLimitCurrency, + _balanceCurrency, + _MAX_FIXED_POINT_FIDELITY + ) ); // Overflow is the balance of this project minus the amount that can still be distributed. @@ -795,7 +804,11 @@ contract JBSingleTokenPaymentTerminalStore3_1_1 is // 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)); + : PRBMath.mulDiv( + _ethOverflow, + 10 ** 18, + prices.priceFor(_projectId, JBCurrencies.ETH, _currency, 18) + ); // Adjust the decimals of the fixed point number if needed to match the target decimals. return diff --git a/contracts/abstract/JBPayoutRedemptionPaymentTerminal3_2.sol b/contracts/abstract/JBPayoutRedemptionPaymentTerminal3_2.sol index d59bc6ef7..271dff2ba 100644 --- a/contracts/abstract/JBPayoutRedemptionPaymentTerminal3_2.sol +++ b/contracts/abstract/JBPayoutRedemptionPaymentTerminal3_2.sol @@ -16,10 +16,10 @@ 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 {IJBPrices3_2} from './../interfaces/IJBPrices3_2.sol'; import {IJBProjects} from './../interfaces/IJBProjects.sol'; import {IJBRedemptionTerminal} from './../interfaces/IJBRedemptionTerminal.sol'; -import {IJBSingleTokenPaymentTerminalStore3_1_1} from './../interfaces/IJBSingleTokenPaymentTerminalStore3_1_1.sol'; +import {IJBSingleTokenPaymentTerminalStore3_2} from './../interfaces/IJBSingleTokenPaymentTerminalStore3_2.sol'; import {IJBSplitAllocator} from './../interfaces/IJBSplitAllocator.sol'; import {JBConstants} from './../libraries/JBConstants.sol'; import {JBCurrencies} from './../libraries/JBCurrencies.sol'; @@ -95,7 +95,7 @@ abstract contract JBPayoutRedemptionPaymentTerminal3_2 is IJBSplitsStore public immutable override splitsStore; /// @notice The contract that exposes price feeds. - IJBPrices public immutable override prices; + IJBPrices3_2 public immutable override prices; /// @notice The contract that stores and manages the terminal's data. address public immutable override store; @@ -131,7 +131,7 @@ abstract contract JBPayoutRedemptionPaymentTerminal3_2 is uint256 _projectId ) external view virtual override returns (uint256) { // Get this terminal's current overflow. - uint256 _overflow = IJBSingleTokenPaymentTerminalStore3_1_1(store).currentOverflowOf( + uint256 _overflow = IJBSingleTokenPaymentTerminalStore3_2(store).currentOverflowOf( this, _projectId ); @@ -148,7 +148,7 @@ abstract contract JBPayoutRedemptionPaymentTerminal3_2 is : PRBMath.mulDiv( _adjustedOverflow, 10 ** decimals, - prices.priceFor(currency, JBCurrencies.ETH, decimals) + prices.priceFor(_projectId, currency, JBCurrencies.ETH, decimals) ); } @@ -212,7 +212,7 @@ abstract contract JBPayoutRedemptionPaymentTerminal3_2 is IJBProjects _projects, IJBDirectory _directory, IJBSplitsStore _splitsStore, - IJBPrices _prices, + IJBPrices3_2 _prices, address _store, address _owner ) @@ -411,7 +411,7 @@ abstract contract JBPayoutRedemptionPaymentTerminal3_2 is if (!_to.acceptsToken(token, _projectId)) revert TERMINAL_TOKENS_INCOMPATIBLE(); // Record the migration in the store. - balance = IJBSingleTokenPaymentTerminalStore3_1_1(store).recordMigration(_projectId); + balance = IJBSingleTokenPaymentTerminalStore3_2(store).recordMigration(_projectId); // Transfer the balance if needed. if (balance != 0) { @@ -646,7 +646,7 @@ abstract contract JBPayoutRedemptionPaymentTerminal3_2 is reclaimAmount, _delegateAllocations, _memo - ) = IJBSingleTokenPaymentTerminalStore3_1_1(store).recordRedemptionFor( + ) = IJBSingleTokenPaymentTerminalStore3_2(store).recordRedemptionFor( _holder, _projectId, _tokenCount, @@ -656,8 +656,7 @@ abstract contract JBPayoutRedemptionPaymentTerminal3_2 is // 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) || + _fundingCycle.redemptionRate() == JBConstants.MAX_REDEMPTION_RATE || _feePercent == 0 ? JBConstants.MAX_FEE_DISCOUNT : _currentFeeDiscount(_projectId, JBFeeType.REDEMPTION); @@ -806,7 +805,7 @@ abstract contract JBPayoutRedemptionPaymentTerminal3_2 is ( JBFundingCycle memory _fundingCycle, uint256 _distributedAmount - ) = IJBSingleTokenPaymentTerminalStore3_1_1(store).recordDistributionFor( + ) = IJBSingleTokenPaymentTerminalStore3_2(store).recordDistributionFor( _projectId, _amount, _currency @@ -924,7 +923,7 @@ abstract contract JBPayoutRedemptionPaymentTerminal3_2 is ( JBFundingCycle memory _fundingCycle, uint256 _distributedAmount - ) = IJBSingleTokenPaymentTerminalStore3_1_1(store).recordUsedAllowanceOf( + ) = IJBSingleTokenPaymentTerminalStore3_2(store).recordUsedAllowanceOf( _projectId, _amount, _currency @@ -1308,10 +1307,7 @@ abstract contract JBPayoutRedemptionPaymentTerminal3_2 is if (_allowanceAmount != 0) _cancelTransferTo(_expectedDestination, _allowanceAmount); // Add undistributed amount back to project's balance. - IJBSingleTokenPaymentTerminalStore3_1_1(store).recordAddedBalanceFor( - _projectId, - _depositAmount - ); + IJBSingleTokenPaymentTerminalStore3_2(store).recordAddedBalanceFor(_projectId, _depositAmount); } /// @notice Contribute tokens to a project. @@ -1355,7 +1351,7 @@ abstract contract JBPayoutRedemptionPaymentTerminal3_2 is _tokenCount, _delegateAllocations, _memo - ) = IJBSingleTokenPaymentTerminalStore3_1_1(store).recordPaymentFrom( + ) = IJBSingleTokenPaymentTerminalStore3_2(store).recordPaymentFrom( _payer, _bundledAmount, _projectId, @@ -1463,7 +1459,7 @@ abstract contract JBPayoutRedemptionPaymentTerminal3_2 is uint256 _refundedFees = _shouldRefundHeldFees ? _refundHeldFees(_projectId, _amount) : 0; // Record the added funds with any refunded fees. - IJBSingleTokenPaymentTerminalStore3_1_1(store).recordAddedBalanceFor( + IJBSingleTokenPaymentTerminalStore3_2(store).recordAddedBalanceFor( _projectId, _amount + _refundedFees ); diff --git a/contracts/interfaces/IJBPayoutRedemptionPaymentTerminal3_2.sol b/contracts/interfaces/IJBPayoutRedemptionPaymentTerminal3_2.sol index 248091340..1e8ff3f61 100644 --- a/contracts/interfaces/IJBPayoutRedemptionPaymentTerminal3_2.sol +++ b/contracts/interfaces/IJBPayoutRedemptionPaymentTerminal3_2.sol @@ -8,7 +8,7 @@ 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 {IJBPrices3_2} from './IJBPrices3_2.sol'; import {IJBProjects} from './IJBProjects.sol'; import {IJBRedemptionDelegate3_1_1} from './IJBRedemptionDelegate3_1_1.sol'; import {IJBRedemptionTerminal} from './IJBRedemptionTerminal.sol'; @@ -170,7 +170,7 @@ interface IJBPayoutRedemptionPaymentTerminal3_2 is function directory() external view returns (IJBDirectory); - function prices() external view returns (IJBPrices); + function prices() external view returns (IJBPrices3_2); function store() external view returns (address); diff --git a/contracts/interfaces/IJBPrices3_2.sol b/contracts/interfaces/IJBPrices3_2.sol new file mode 100644 index 000000000..9c034f4a2 --- /dev/null +++ b/contracts/interfaces/IJBPrices3_2.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IJBPriceFeed} from './IJBPriceFeed.sol'; +import {IJBProjects} from './IJBProjects.sol'; + +interface IJBPrices3_2 { + event AddFeed( + uint256 indexed projectId, + uint256 indexed currency, + uint256 indexed base, + IJBPriceFeed feed + ); + + function DEFAULT_PROJECT_ID() external view returns (uint256); + + function projects() external view returns (IJBProjects); + + function feedFor( + uint256 projectId, + uint256 currency, + uint256 base + ) external view returns (IJBPriceFeed); + + function priceFor( + uint256 projectId, + uint256 currency, + uint256 base, + uint256 decimals + ) external view returns (uint256); + + function addFeedFor( + uint256 _projectId, + uint256 currency, + uint256 base, + IJBPriceFeed priceFeed + ) external; +} diff --git a/contracts/interfaces/IJBSingleTokenPaymentTerminalStore3_2.sol b/contracts/interfaces/IJBSingleTokenPaymentTerminalStore3_2.sol index df2db4f6d..39d97925d 100644 --- a/contracts/interfaces/IJBSingleTokenPaymentTerminalStore3_2.sol +++ b/contracts/interfaces/IJBSingleTokenPaymentTerminalStore3_2.sol @@ -7,7 +7,7 @@ import {JBRedemptionDelegateAllocation3_1_1} from './../structs/JBRedemptionDele import {JBTokenAmount} from './../structs/JBTokenAmount.sol'; import {IJBDirectory} from './IJBDirectory.sol'; import {IJBFundingCycleStore} from './IJBFundingCycleStore.sol'; -import {IJBPrices} from './IJBPrices.sol'; +import {IJBPrices3_2} from './IJBPrices3_2.sol'; import {IJBSingleTokenPaymentTerminal} from './IJBSingleTokenPaymentTerminal.sol'; interface IJBSingleTokenPaymentTerminalStore3_2 { @@ -15,7 +15,7 @@ interface IJBSingleTokenPaymentTerminalStore3_2 { function directory() external view returns (IJBDirectory); - function prices() external view returns (IJBPrices); + function prices() external view returns (IJBPrices3_2); function balanceOf( IJBSingleTokenPaymentTerminal terminal, diff --git a/contracts/libraries/JBOperations2.sol b/contracts/libraries/JBOperations2.sol new file mode 100644 index 000000000..158c7fb65 --- /dev/null +++ b/contracts/libraries/JBOperations2.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +library JBOperations2 { + uint256 public constant ADD_PRICE_FEED = 19; +}