diff --git a/.gitmodules b/.gitmodules index 8fa5be48..853782e5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "lib/buffer"] path = lib/buffer url = https://github.com/ensdomains/buffer +[submodule "lib/pyth-sdk-solidity"] + path = lib/pyth-sdk-solidity + url = https://github.com/pyth-network/pyth-sdk-solidity diff --git a/lib/pyth-sdk-solidity b/lib/pyth-sdk-solidity new file mode 160000 index 00000000..11d6bcfc --- /dev/null +++ b/lib/pyth-sdk-solidity @@ -0,0 +1 @@ +Subproject commit 11d6bcfc2e56885535a9a8e3c8417847cb20be14 diff --git a/remappings.txt b/remappings.txt index 41209dd1..c7a9b45f 100644 --- a/remappings.txt +++ b/remappings.txt @@ -4,4 +4,5 @@ forge-std/=lib/forge-std/src/ @openzeppelin/=lib/openzeppelin-contracts/ contract-template/=lib/contract-template/src/ @ensdomains/ens-contracts/=lib/ens-contracts/contracts/ -@ensdomains/buffer/=lib/buffer/ \ No newline at end of file +@ensdomains/buffer/=lib/buffer/ +@pythnetwork/=lib/pyth-sdk-solidity/ diff --git a/src/RNSDomainPrice.sol b/src/RNSDomainPrice.sol new file mode 100644 index 00000000..ba64d3e3 --- /dev/null +++ b/src/RNSDomainPrice.sol @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { AccessControlEnumerable } from "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { IPyth, PythStructs } from "@pythnetwork/IPyth.sol"; +import { INSAuction } from "./interfaces/INSAuction.sol"; +import { INSDomainPrice } from "./interfaces/INSDomainPrice.sol"; +import { PeriodScaler, LibPeriodScaler, Math } from "src/libraries/math/PeriodScalingUtils.sol"; +import { TimestampWrapper } from "./libraries/TimestampWrapperUtils.sol"; +import { LibRNSDomain } from "./libraries/LibRNSDomain.sol"; +import { PythConverter } from "./libraries/pyth/PythConverter.sol"; + +contract RNSDomainPrice is Initializable, AccessControlEnumerable, INSDomainPrice { + using LibRNSDomain for string; + using LibPeriodScaler for PeriodScaler; + using PythConverter for PythStructs.Price; + + /// @inheritdoc INSDomainPrice + uint8 public constant USD_DECIMALS = 18; + /// @inheritdoc INSDomainPrice + uint64 public constant MAX_PERCENTAGE = 100_00; + /// @inheritdoc INSDomainPrice + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + + /// @dev Gap for upgradeability. + uint256[50] private ____gap; + + /// @dev Pyth oracle contract + IPyth internal _pyth; + /// @dev RNSAuction contract + INSAuction internal _auction; + /// @dev Extra fee for renewals based on the current domain price. + uint256 internal _taxRatio; + /// @dev Max length of the renewal fee + uint256 internal _rnfMaxLength; + /// @dev Max acceptable age of the price oracle request + uint256 internal _maxAcceptableAge; + /// @dev Price feed ID on Pyth for RON/USD + bytes32 internal _pythIdForRONUSD; + /// @dev The percentage scale from domain price each period + PeriodScaler internal _dpDownScaler; + + /// @dev Mapping from domain length => renewal fee in USD + mapping(uint256 length => uint256 usdPrice) internal _rnFee; + /// @dev Mapping from name => domain price in USD + mapping(bytes32 lbHash => TimestampWrapper usdPrice) internal _dp; + /// @dev Mapping from name => inverse bitwise of renewal fee overriding. + mapping(bytes32 lbHash => uint256 usdPrice) internal _rnFeeOverriding; + + constructor() payable { + _disableInitializers(); + } + + function initialize( + address admin, + address[] calldata operators, + RenewalFee[] calldata renewalFees, + uint256 taxRatio, + PeriodScaler calldata domainPriceScaleRule, + IPyth pyth, + INSAuction auction, + uint256 maxAcceptableAge, + bytes32 pythIdForRONUSD + ) external initializer { + uint256 length = operators.length; + bytes32 operatorRole; + + for (uint256 i; i < length;) { + _setupRole(operatorRole, operators[i]); + + unchecked { + ++i; + } + } + _auction = auction; + _setupRole(DEFAULT_ADMIN_ROLE, admin); + _setRenewalFeeByLengths(renewalFees); + _setTaxRatio(taxRatio); + _setDomainPriceScaleRule(domainPriceScaleRule); + _setPythOracleConfig(pyth, maxAcceptableAge, pythIdForRONUSD); + } + + /** + * @inheritdoc INSDomainPrice + */ + function getPythOracleConfig() external view returns (IPyth pyth, uint256 maxAcceptableAge, bytes32 pythIdForRONUSD) { + return (_pyth, _maxAcceptableAge, _pythIdForRONUSD); + } + + /** + * @inheritdoc INSDomainPrice + */ + function setPythOracleConfig(IPyth pyth, uint256 maxAcceptableAge, bytes32 pythIdForRONUSD) + external + onlyRole(DEFAULT_ADMIN_ROLE) + { + _setPythOracleConfig(pyth, maxAcceptableAge, pythIdForRONUSD); + } + + /** + * @inheritdoc INSDomainPrice + */ + function getRenewalFeeByLengths() external view returns (RenewalFee[] memory renewalFees) { + uint256 rnfMaxLength = _rnfMaxLength; + renewalFees = new RenewalFee[](rnfMaxLength); + uint256 len; + + for (uint256 i; i < rnfMaxLength;) { + unchecked { + len = i + 1; + renewalFees[i].labelLength = len; + renewalFees[i].fee = _rnFee[len]; + ++i; + } + } + } + + /** + * @inheritdoc INSDomainPrice + */ + function setRenewalFeeByLengths(RenewalFee[] calldata renewalFees) external onlyRole(DEFAULT_ADMIN_ROLE) { + _setRenewalFeeByLengths(renewalFees); + } + + /** + * @inheritdoc INSDomainPrice + */ + function getTaxRatio() external view returns (uint256 ratio) { + return _taxRatio; + } + + /** + * @inheritdoc INSDomainPrice + */ + function setTaxRatio(uint256 ratio) external onlyRole(DEFAULT_ADMIN_ROLE) { + _setTaxRatio(ratio); + } + + /** + * @inheritdoc INSDomainPrice + */ + function getScaleDownRuleForDomainPrice() external view returns (PeriodScaler memory scaleRule) { + return _dpDownScaler; + } + + /** + * @inheritdoc INSDomainPrice + */ + function setScaleDownRuleForDomainPrice(PeriodScaler calldata scaleRule) external onlyRole(DEFAULT_ADMIN_ROLE) { + _setDomainPriceScaleRule(scaleRule); + } + + /** + * @inheritdoc INSDomainPrice + */ + function getOverriddenRenewalFee(string calldata label) external view returns (uint256 usdFee) { + usdFee = _rnFeeOverriding[label.hashLabel()]; + if (usdFee == 0) revert RenewalFeeIsNotOverriden(); + return ~usdFee; + } + + /** + * @inheritdoc INSDomainPrice + */ + function bulkOverrideRenewalFees(bytes32[] calldata lbHashes, uint256[] calldata usdPrices) + external + onlyRole(OPERATOR_ROLE) + { + uint256 length = lbHashes.length; + if (length == 0 || length != usdPrices.length) revert InvalidArrayLength(); + uint256 inverseBitwise; + address operator = _msgSender(); + + for (uint256 i; i < length;) { + inverseBitwise = ~usdPrices[i]; + _rnFeeOverriding[lbHashes[i]] = inverseBitwise; + emit RenewalFeeOverridingUpdated(operator, lbHashes[i], inverseBitwise); + + unchecked { + ++i; + } + } + } + + /** + * @inheritdoc INSDomainPrice + */ + function bulkTrySetDomainPrice( + bytes32[] calldata lbHashes, + uint256[] calldata ronPrices, + bytes32[] calldata proofHashes, + uint256[] calldata setTypes + ) external onlyRole(OPERATOR_ROLE) returns (bool[] memory updated) { + uint256 length = _requireBulkSetDomainPriceArgumentsValid(lbHashes, ronPrices, proofHashes, setTypes); + address operator = _msgSender(); + updated = new bool[](length); + + for (uint256 i; i < length;) { + updated[i] = _setDomainPrice(operator, lbHashes[i], ronPrices[i], proofHashes[i], setTypes[i], false); + + unchecked { + ++i; + } + } + } + + /** + * @inheritdoc INSDomainPrice + */ + function bulkSetDomainPrice( + bytes32[] calldata lbHashes, + uint256[] calldata ronPrices, + bytes32[] calldata proofHashes, + uint256[] calldata setTypes + ) external onlyRole(OPERATOR_ROLE) { + uint256 length = _requireBulkSetDomainPriceArgumentsValid(lbHashes, ronPrices, proofHashes, setTypes); + address operator = _msgSender(); + + for (uint256 i; i < length;) { + _setDomainPrice(operator, lbHashes[i], ronPrices[i], proofHashes[i], setTypes[i], true); + unchecked { + ++i; + } + } + } + + /** + * @inheritdoc INSDomainPrice + */ + function getDomainPrice(string memory label) public view returns (uint256 usdPrice, uint256 ronPrice) { + usdPrice = _getDomainPrice(label.hashLabel()); + ronPrice = convertUSDToRON(usdPrice); + } + + /** + * @inheritdoc INSDomainPrice + */ + function getRenewalFee(string memory label, uint256 duration) + public + view + returns (UnitPrice memory basePrice, UnitPrice memory tax) + { + uint256 nameLen = label.strlen(); + bytes32 lbHash = label.hashLabel(); + uint256 overriddenRenewalFee = _rnFeeOverriding[lbHash]; + + if (overriddenRenewalFee != 0) { + basePrice.usd = duration * ~overriddenRenewalFee; + } else { + uint256 renewalFeeByLength = _rnFee[Math.min(nameLen, _rnfMaxLength)]; + basePrice.usd = duration * renewalFeeByLength; + // tax is added of name is reserved for auction + if (_auction.reserved(LibRNSDomain.toId(LibRNSDomain.RON_ID, label))) { + tax.usd = Math.mulDiv(_taxRatio, _getDomainPrice(lbHash), MAX_PERCENTAGE); + } + } + + tax.ron = convertUSDToRON(tax.usd); + basePrice.ron = convertUSDToRON(basePrice.usd); + } + + /** + * @inheritdoc INSDomainPrice + */ + function convertUSDToRON(uint256 usdWei) public view returns (uint256 ronWei) { + return _pyth.getPriceNoOlderThan(_pythIdForRONUSD, _maxAcceptableAge).inverse({ expo: -18 }).mul({ + inpWei: usdWei, + inpDecimals: int32(uint32(USD_DECIMALS)), + outDecimals: 18 + }); + } + + /** + * @inheritdoc INSDomainPrice + */ + function convertRONToUSD(uint256 ronWei) public view returns (uint256 usdWei) { + return _pyth.getPriceNoOlderThan(_pythIdForRONUSD, _maxAcceptableAge).mul({ + inpWei: ronWei, + inpDecimals: 18, + outDecimals: int32(uint32(USD_DECIMALS)) + }); + } + + /** + * @dev Reverts if the arguments of the method {bulkSetDomainPrice} is invalid. + */ + function _requireBulkSetDomainPriceArgumentsValid( + bytes32[] calldata lbHashes, + uint256[] calldata ronPrices, + bytes32[] calldata proofHashes, + uint256[] calldata setTypes + ) internal pure returns (uint256 length) { + length = lbHashes.length; + if (length == 0 || ronPrices.length != length || proofHashes.length != length || setTypes.length != length) { + revert InvalidArrayLength(); + } + } + + /** + * @dev Helper method to set domain price. + * + * Emits an event {DomainPriceUpdated} optionally. + */ + function _setDomainPrice( + address operator, + bytes32 lbHash, + uint256 ronPrice, + bytes32 proofHash, + uint256 setType, + bool forced + ) internal returns (bool updated) { + uint256 usdPrice = convertRONToUSD(ronPrice); + TimestampWrapper storage dp = _dp[lbHash]; + updated = forced || dp.value < usdPrice; + + if (updated) { + dp.value = usdPrice; + dp.timestamp = block.timestamp; + emit DomainPriceUpdated(operator, lbHash, usdPrice, proofHash, setType); + } + } + + /** + * @dev Sets renewal reservation ratio. + * + * Emits an event {TaxRatioUpdated}. + */ + function _setTaxRatio(uint256 ratio) internal { + _taxRatio = ratio; + emit TaxRatioUpdated(_msgSender(), ratio); + } + + /** + * @dev Sets domain price scale rule. + * + * Emits events {DomainPriceScaleRuleUpdated}. + */ + function _setDomainPriceScaleRule(PeriodScaler calldata domainPriceScaleRule) internal { + _dpDownScaler = domainPriceScaleRule; + emit DomainPriceScaleRuleUpdated(_msgSender(), domainPriceScaleRule.ratio, domainPriceScaleRule.period); + } + + /** + * @dev Sets renewal fee. + * + * Emits events {RenewalFeeByLengthUpdated}. + * Emits an event {MaxRenewalFeeLengthUpdated} optionally. + */ + function _setRenewalFeeByLengths(RenewalFee[] calldata renewalFees) internal { + address operator = _msgSender(); + RenewalFee memory renewalFee; + uint256 length = renewalFees.length; + uint256 maxRenewalFeeLength = _rnfMaxLength; + + for (uint256 i; i < length;) { + renewalFee = renewalFees[i]; + maxRenewalFeeLength = Math.max(maxRenewalFeeLength, renewalFee.labelLength); + _rnFee[renewalFee.labelLength] = renewalFee.fee; + emit RenewalFeeByLengthUpdated(operator, renewalFee.labelLength, renewalFee.fee); + + unchecked { + ++i; + } + } + + if (maxRenewalFeeLength != _rnfMaxLength) { + _rnfMaxLength = maxRenewalFeeLength; + emit MaxRenewalFeeLengthUpdated(operator, maxRenewalFeeLength); + } + } + + /** + * @dev Sets Pyth Oracle config. + * + * Emits events {PythOracleConfigUpdated}. + */ + function _setPythOracleConfig(IPyth pyth, uint256 maxAcceptableAge, bytes32 pythIdForRONUSD) internal { + _pyth = pyth; + _maxAcceptableAge = maxAcceptableAge; + _pythIdForRONUSD = pythIdForRONUSD; + emit PythOracleConfigUpdated(_msgSender(), pyth, maxAcceptableAge, pythIdForRONUSD); + } + + /** + * @dev Returns the current domain price applied the business rule: deduced x% each y seconds. + */ + function _getDomainPrice(bytes32 lbHash) internal view returns (uint256) { + TimestampWrapper storage dp = _dp[lbHash]; + uint256 lastSyncedAt = dp.timestamp; + if (lastSyncedAt == 0) return 0; + + uint256 passedDuration = block.timestamp - lastSyncedAt; + return _dpDownScaler.scaleDown({ v: dp.value, maxR: MAX_PERCENTAGE, dur: passedDuration }); + } +} diff --git a/src/interfaces/INSDomainPrice.sol b/src/interfaces/INSDomainPrice.sol new file mode 100644 index 00000000..735a97fd --- /dev/null +++ b/src/interfaces/INSDomainPrice.sol @@ -0,0 +1,210 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { PeriodScaler } from "../libraries/math/PeriodScalingUtils.sol"; +import { IPyth } from "@pythnetwork/IPyth.sol"; + +interface INSDomainPrice { + error InvalidArrayLength(); + error RenewalFeeIsNotOverriden(); + + struct RenewalFee { + uint256 labelLength; + uint256 fee; + } + + struct UnitPrice { + uint256 usd; + uint256 ron; + } + + /// @dev Emitted when the renewal reservation ratio is updated. + event TaxRatioUpdated(address indexed operator, uint256 indexed ratio); + /// @dev Emitted when the maximum length of renewal fee is updated. + event MaxRenewalFeeLengthUpdated(address indexed operator, uint256 indexed maxLength); + /// @dev Emitted when the renew fee is updated. + event RenewalFeeByLengthUpdated(address indexed operator, uint256 indexed labelLength, uint256 renewalFee); + /// @dev Emitted when the renew fee of a domain is overridden. Value of `inverseRenewalFee` is 0 when not overridden. + event RenewalFeeOverridingUpdated(address indexed operator, bytes32 indexed labelHash, uint256 inverseRenewalFee); + + /// @dev Emitted when the domain price is updated. + event DomainPriceUpdated( + address indexed operator, bytes32 indexed labelHash, uint256 price, bytes32 indexed proofHash, uint256 setType + ); + /// @dev Emitted when the rule to rescale domain price is updated. + event DomainPriceScaleRuleUpdated(address indexed operator, uint192 ratio, uint64 period); + + /// @dev Emitted when the Pyth Oracle config is updated. + event PythOracleConfigUpdated( + address indexed operator, IPyth indexed pyth, uint256 maxAcceptableAge, bytes32 indexed pythIdForRONUSD + ); + + /** + * @dev Returns the Pyth oracle config. + */ + function getPythOracleConfig() external view returns (IPyth pyth, uint256 maxAcceptableAge, bytes32 pythIdForRONUSD); + + /** + * @dev Sets the Pyth oracle config. + * + * Requirements: + * - The method caller is admin. + * + * Emits events {PythOracleConfigUpdated}. + */ + function setPythOracleConfig(IPyth pyth, uint256 maxAcceptableAge, bytes32 pythIdForRONUSD) external; + + /** + * @dev Returns the percentage to scale from domain price each period. + */ + function getScaleDownRuleForDomainPrice() external view returns (PeriodScaler memory dpScaleRule); + + /** + * @dev Sets the percentage to scale from domain price each period. + * + * Requirements: + * - The method caller is admin. + * + * Emits events {DomainPriceScaleRuleUpdated}. + * + * @notice Applies for the business rule: -x% each y seconds. + */ + function setScaleDownRuleForDomainPrice(PeriodScaler calldata scaleRule) external; + + /** + * @dev Returns the renewal fee by lengths. + */ + function getRenewalFeeByLengths() external view returns (RenewalFee[] memory renewalFees); + + /** + * @dev Sets the renewal fee by lengths + * + * Requirements: + * - The method caller is admin. + * + * Emits events {RenewalFeeByLengthUpdated}. + * Emits an event {MaxRenewalFeeLengthUpdated} optionally. + */ + function setRenewalFeeByLengths(RenewalFee[] calldata renewalFees) external; + + /** + * @dev Returns tax ratio. + */ + function getTaxRatio() external view returns (uint256 taxRatio); + + /** + * @dev Sets renewal reservation ratio. + * + * Requirements: + * - The method caller is admin. + * + * Emits an event {TaxRatioUpdated}. + */ + function setTaxRatio(uint256 ratio) external; + + /** + * @dev Return the domain price. + * @param label The domain label to register (Eg, 'foo' for 'foo.ron'). + */ + function getDomainPrice(string memory label) external view returns (uint256 usdPrice, uint256 ronPrice); + + /** + * @dev Returns the renewal fee in USD and RON. + * @param label The domain label to register (Eg, 'foo' for 'foo.ron'). + * @param duration Amount of second(s). + */ + function getRenewalFee(string calldata label, uint256 duration) + external + view + returns (UnitPrice memory basePrice, UnitPrice memory tax); + + /** + * @dev Returns the renewal fee of a label. Reverts if not overridden. + * @notice This method is to help developers check the domain renewal fee overriding. Consider using method + * {getRenewalFee} instead for full handling of renewal fees. + */ + function getOverriddenRenewalFee(string memory label) external view returns (uint256 usdFee); + + /** + * @dev Bulk override renewal fees. + * + * Requirements: + * - The method caller is operator. + * - The input array lengths must be larger than 0 and the same. + * + * Emits events {RenewalFeeOverridingUpdated}. + * + * @param lbHashes Array of label hashes. (Eg, ['foo'].map(keccak256) for 'foo.ron') + * @param usdPrices Array of prices in USD. Leave 2^256 - 1 to remove overriding. + */ + function bulkOverrideRenewalFees(bytes32[] calldata lbHashes, uint256[] calldata usdPrices) external; + + /** + * @dev Bulk try to set domain prices. Returns a boolean array indicating whether domain prices at the corresponding + * indexes if set or not. + * + * Requirements: + * - The method caller is operator. + * - The input array lengths must be larger than 0 and the same. + * - The price should be larger than current domain price or it will not be updated. + * + * Emits events {DomainPriceUpdated} optionally. + * + * @param lbHashes Array of label hashes. (Eg, ['foo'].map(keccak256) for 'foo.ron') + * @param ronPrices Array of prices in (W)RON token. + * @param proofHashes Array of proof hashes. + * @param setTypes Array of update types from the operator service. + */ + function bulkTrySetDomainPrice( + bytes32[] calldata lbHashes, + uint256[] calldata ronPrices, + bytes32[] calldata proofHashes, + uint256[] calldata setTypes + ) external returns (bool[] memory updated); + + /** + * @dev Bulk override domain prices. + * + * Requirements: + * - The method caller is operator. + * - The input array lengths must be larger than 0 and the same. + * + * Emits events {DomainPriceUpdated}. + * + * @param lbHashes Array of label hashes. (Eg, ['foo'].map(keccak256) for 'foo.ron') + * @param ronPrices Array of prices in (W)RON token. + * @param proofHashes Array of proof hashes. + * @param setTypes Array of update types from the operator service. + */ + function bulkSetDomainPrice( + bytes32[] calldata lbHashes, + uint256[] calldata ronPrices, + bytes32[] calldata proofHashes, + uint256[] calldata setTypes + ) external; + + /** + * @dev Returns the converted amount from USD to RON. + */ + function convertUSDToRON(uint256 usdAmount) external view returns (uint256 ronAmount); + + /** + * @dev Returns the converted amount from RON to USD. + */ + function convertRONToUSD(uint256 ronAmount) external view returns (uint256 usdAmount); + + /** + * @dev Value equals to keccak256("OPERATOR_ROLE"). + */ + function OPERATOR_ROLE() external pure returns (bytes32); + + /** + * @dev Max percentage 100%. Values [0; 100_00] reflexes [0; 100%] + */ + function MAX_PERCENTAGE() external pure returns (uint64); + + /** + * @dev Decimal for USD. + */ + function USD_DECIMALS() external pure returns (uint8); +} diff --git a/src/libraries/LibRNSDomain.sol b/src/libraries/LibRNSDomain.sol index 9baf281b..5ced21a7 100644 --- a/src/libraries/LibRNSDomain.sol +++ b/src/libraries/LibRNSDomain.sol @@ -21,4 +21,35 @@ library LibRNSDomain { hashed := keccak256(add(label, 32), mload(label)) } } + + /** + * @dev Returns the length of a given string + * + * @param s The string to measure the length of + * @return The length of the input string + */ + function strlen(string memory s) internal pure returns (uint256) { + unchecked { + uint256 i; + uint256 len; + uint256 bytelength = bytes(s).length; + for (len; i < bytelength; len++) { + bytes1 b = bytes(s)[i]; + if (b < 0x80) { + i += 1; + } else if (b < 0xE0) { + i += 2; + } else if (b < 0xF0) { + i += 3; + } else if (b < 0xF8) { + i += 4; + } else if (b < 0xFC) { + i += 5; + } else { + i += 6; + } + } + return len; + } + } } diff --git a/src/libraries/TimestampWrapperUtils.sol b/src/libraries/TimestampWrapperUtils.sol new file mode 100644 index 00000000..d2228eca --- /dev/null +++ b/src/libraries/TimestampWrapperUtils.sol @@ -0,0 +1,7 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +struct TimestampWrapper { + uint256 value; + uint256 timestamp; +} diff --git a/src/libraries/math/PeriodScalingUtils.sol b/src/libraries/math/PeriodScalingUtils.sol new file mode 100644 index 00000000..b2dcf81c --- /dev/null +++ b/src/libraries/math/PeriodScalingUtils.sol @@ -0,0 +1,42 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { PowMath } from "./PowMath.sol"; + +struct PeriodScaler { + uint192 ratio; + uint64 period; +} + +library LibPeriodScaler { + using PowMath for uint256; + + error PeriodNumOverflowedUint16(uint256 n); + + /// @dev The precision number of calculation is 2 + uint256 public constant MAX_PERCENTAGE = 100_00; + + /** + * @dev Scales down the input value `v` for a percentage of `self.ratio` each period `self.period`. + * Reverts if the passed period is larger than 2^16 - 1. + * + * @param self The period scaler with specific period and ratio + * @param v The original value to scale based on the rule `self` + * @param maxR The maximum value of 100%. Eg, if the `self.ratio` in range of [0;100_00] reflexes 0-100%, this param + * must be 100_00 + * @param dur The passed duration in the same uint with `self.period` + */ + function scaleDown(PeriodScaler memory self, uint256 v, uint64 maxR, uint256 dur) internal pure returns (uint256 rs) { + uint256 n = dur / uint256(self.period); + if (n == 0 || self.ratio == 0) return v; + if (maxR == self.ratio) return 0; + if (n > type(uint16).max) revert PeriodNumOverflowedUint16(n); + + unchecked { + // Normalizes the input ratios to be in range of [0;MAX_PERCENTAGE] + uint256 p = Math.mulDiv(maxR - self.ratio, MAX_PERCENTAGE, maxR); + return v.mulDiv({ y: p, d: MAX_PERCENTAGE, n: uint16(n) }); + } + } +} diff --git a/src/libraries/math/PowMath.sol b/src/libraries/math/PowMath.sol new file mode 100644 index 00000000..a156278c --- /dev/null +++ b/src/libraries/math/PowMath.sol @@ -0,0 +1,130 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + +library PowMath { + using Math for uint256; + using SafeMath for uint256; + + /** + * @dev Negative exponent n for x*10^n. + */ + function exp10(uint256 x, int32 n) internal pure returns (uint256) { + if (n < 0) { + return x / 10 ** uint32(-n); + } else if (n > 0) { + return x * 10 ** uint32(n); + } else { + return x; + } + } + + /** + * @dev Calculates floor(x * (y / d)**n) with full precision. + */ + function mulDiv(uint256 x, uint256 y, uint256 d, uint16 n) internal pure returns (uint256 r) { + unchecked { + if (y == d || n == 0) return x; + r = x; + + bool ok; + uint256 r_; + uint16 nd_; + + { + uint16 ye = uint16(Math.min(n, findMaxExponent(y))); + while (ye > 0) { + (ok, r_) = r.tryMul(y ** ye); + if (ok) { + r = r_; + n -= ye; + nd_ += ye; + } + ye = uint16(Math.min(ye / 2, n)); + } + } + + while (n > 0) { + (ok, r_) = r.tryMul(y); + if (ok) { + r = r_; + n--; + nd_++; + } else if (nd_ > 0) { + r /= d; + nd_--; + } else { + r = r.mulDiv(y, d); + n--; + } + } + + uint16 de = findMaxExponent(d); + while (nd_ > 0) { + uint16 e = uint16(Math.min(de, nd_)); + r /= d ** e; + nd_ -= e; + } + } + } + + /** + * @dev Calculates floor(x * (y / d)**n) with low precision. + */ + function mulDivLowPrecision(uint256 x, uint256 y, uint256 d, uint16 n) internal pure returns (uint256) { + return uncheckedMulDiv(x, y, d, n, findMaxExponent(Math.max(y, d))); + } + + /** + * @dev Aggregated calculate multiplications. + * ``` + * r = x*(y/d)^k + * = \prod(x*(y/d)^{k_i}) \ where \ sum(k_i) = k + * ``` + */ + function uncheckedMulDiv(uint256 x, uint256 y, uint256 d, uint16 n, uint16 maxE) internal pure returns (uint256 r) { + unchecked { + r = x; + uint16 e; + while (n > 0) { + e = uint16(Math.min(n, maxE)); + r = r.mulDiv(y ** e, d ** e); + n -= e; + } + } + } + + /** + * @dev Returns the largest exponent `k` where, x^k <= 2^256-1 + * Note: n = Surd[2^256-1,k] + * = 10^( log2(2^256-1) / k * log10(2) ) + */ + function findMaxExponent(uint256 x) internal pure returns (uint16 k) { + if (x < 3) k = 255; + else if (x < 4) k = 128; + else if (x < 16) k = 64; + else if (x < 256) k = 32; + else if (x < 7132) k = 20; + else if (x < 11376) k = 19; + else if (x < 19113) k = 18; + else if (x < 34132) k = 17; + else if (x < 65536) k = 16; + else if (x < 137271) k = 15; + else if (x < 319558) k = 14; + else if (x < 847180) k = 13; + else if (x < 2642246) k = 12; + else if (x < 10134189) k = 11; + else if (x < 50859009) k = 10; + else if (x < 365284285) k = 9; + else if (x < 4294967296) k = 8; + else if (x < 102116749983) k = 7; + else if (x < 6981463658332) k = 6; + else if (x < 2586638741762875) k = 5; + else if (x < 18446744073709551616) k = 4; + else if (x < 48740834812604276470692695) k = 3; + else if (x < 340282366920938463463374607431768211456) k = 2; + else k = 1; + } +} diff --git a/src/libraries/pyth/PythConverter.sol b/src/libraries/pyth/PythConverter.sol new file mode 100644 index 00000000..efc176a1 --- /dev/null +++ b/src/libraries/pyth/PythConverter.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { PythStructs } from "@pythnetwork/PythStructs.sol"; +import { PowMath } from "../math/PowMath.sol"; + +library PythConverter { + error ErrExponentTooLarge(int32 expo); + error ErrComputedPriceTooLarge(int32 expo1, int32 expo2, int64 price1); + + /** + * @dev Multiples and converts the price into token wei with decimals `outDecimals`. + */ + function mul(PythStructs.Price memory self, uint256 inpWei, int32 inpDecimals, int32 outDecimals) + internal + pure + returns (uint256 outWei) + { + return Math.mulDiv( + inpWei, PowMath.exp10(uint256(int256(self.price)), outDecimals + self.expo), PowMath.exp10(1, inpDecimals) + ); + } + + /** + * @dev Inverses token price of tokenA/tokenB to tokenB/tokenA. + */ + function inverse(PythStructs.Price memory self, int32 expo) internal pure returns (PythStructs.Price memory outPrice) { + uint256 exp10p1 = PowMath.exp10(1, -self.expo); + if (exp10p1 > uint256(type(int256).max)) revert ErrExponentTooLarge(self.expo); + uint256 exp10p2 = PowMath.exp10(1, -expo); + if (exp10p2 > uint256(type(int256).max)) revert ErrExponentTooLarge(expo); + int256 price = (int256(exp10p1) * int256(exp10p2)) / self.price; + if (price > type(int64).max) revert ErrComputedPriceTooLarge(self.expo, expo, self.price); + + return PythStructs.Price({ price: int64(price), conf: self.conf, expo: expo, publishTime: self.publishTime }); + } +}