From e80c25136e7085bf7e6894915e608de383713309 Mon Sep 17 00:00:00 2001 From: Tu Do <18521578@gm.uit.edu.vn> Date: Thu, 12 Oct 2023 15:44:46 +0700 Subject: [PATCH 1/4] feat: add`RNSDomainPrice` contract (#19) * feat: add RNSDomainPrice * forge install: pyth-sdk-solidity v2.2.0 --- .gitmodules | 3 + lib/pyth-sdk-solidity | 1 + remappings.txt | 3 +- src/RNSDomainPrice.sol | 396 ++++++++++++++++++++++ src/interfaces/INSAuction.sol | 200 +++++++++++ src/interfaces/INSDomainPrice.sol | 205 +++++++++++ src/libraries/LibEventRange.sol | 37 ++ src/libraries/LibRNSDomain.sol | 55 +++ src/libraries/TimestampWrapperUtils.sol | 7 + src/libraries/math/PeriodScalingUtils.sol | 42 +++ src/libraries/math/PowMath.sol | 130 +++++++ src/libraries/pyth/PythConverter.sol | 38 +++ 12 files changed, 1116 insertions(+), 1 deletion(-) create mode 160000 lib/pyth-sdk-solidity create mode 100644 src/RNSDomainPrice.sol create mode 100644 src/interfaces/INSAuction.sol create mode 100644 src/interfaces/INSDomainPrice.sol create mode 100644 src/libraries/LibEventRange.sol create mode 100644 src/libraries/LibRNSDomain.sol create mode 100644 src/libraries/TimestampWrapperUtils.sol create mode 100644 src/libraries/math/PeriodScalingUtils.sol create mode 100644 src/libraries/math/PowMath.sol create mode 100644 src/libraries/pyth/PythConverter.sol diff --git a/.gitmodules b/.gitmodules index 544667fe..15220f8c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "lib/contract-template"] path = lib/contract-template url = https://github.com/axieinfinity/contract-template +[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 e2c6633c..20df439b 100644 --- a/remappings.txt +++ b/remappings.txt @@ -2,4 +2,5 @@ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ @openzeppelin/=lib/openzeppelin-contracts/ -contract-template/=lib/contract-template/src/ \ No newline at end of file +contract-template/=lib/contract-template/src/ +@pythnetwork/=lib/pyth-sdk-solidity/ \ No newline at end of file diff --git a/src/RNSDomainPrice.sol b/src/RNSDomainPrice.sol new file mode 100644 index 00000000..382d14c3 --- /dev/null +++ b/src/RNSDomainPrice.sol @@ -0,0 +1,396 @@ +// 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 (uint256 usdPrice, uint256 ronPrice) + { + uint256 nameLen = label.strlen(); + bytes32 lbHash = label.hashLabel(); + uint256 overriddenRenewalFee = _rnFeeOverriding[lbHash]; + + if (overriddenRenewalFee != 0) { + usdPrice = duration * ~overriddenRenewalFee; + } else { + uint256 renewalFeeByLength = _rnFee[Math.min(nameLen, _rnfMaxLength)]; + usdPrice = duration * renewalFeeByLength; + // tax is added of name is reserved for auction + if (_auction.reserved(LibRNSDomain.toId(LibRNSDomain.RON_ID, label))) { + usdPrice += Math.mulDiv(_taxRatio, _getDomainPrice(lbHash), MAX_PERCENTAGE); + } + } + + ronPrice = convertUSDToRON(usdPrice); + } + + /** + * @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/INSAuction.sol b/src/interfaces/INSAuction.sol new file mode 100644 index 00000000..878b914b --- /dev/null +++ b/src/interfaces/INSAuction.sol @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { INSUnified } from "./INSUnified.sol"; +import { EventRange } from "../libraries/LibEventRange.sol"; + +interface INSAuction { + error NotYetEnded(); + error NoOneBidded(); + error NullAssignment(); + error AlreadyBidding(); + error RatioIsTooLarge(); + error NameNotReserved(); + error InvalidEventRange(); + error QueryIsNotInPeriod(); + error InsufficientAmount(); + error InvalidArrayLength(); + error BidderCannotReceiveRON(); + error EventIsNotCreatedOrAlreadyStarted(); + + struct Bid { + address payable bidder; + uint256 price; + uint256 timestamp; + bool claimed; + } + + struct DomainAuction { + bytes32 auctionId; + uint256 startingPrice; + Bid bid; + } + + /// @dev Emitted when an auction is set. + event AuctionEventSet(bytes32 indexed auctionId, EventRange range); + /// @dev Emitted when the labels are listed for auction. + event LabelsListed(bytes32 indexed auctionId, uint256[] ids, uint256[] startingPrices); + /// @dev Emitted when a bid is placed for a name. + event BidPlaced( + bytes32 indexed auctionId, + uint256 indexed id, + uint256 price, + address payable bidder, + uint256 previousPrice, + address previousBidder + ); + /// @dev Emitted when the treasury is updated. + event TreasuryUpdated(address indexed addr); + /// @dev Emitted when bid gap ratio is updated. + event BidGapRatioUpdated(uint256 ratio); + + /** + * @dev The maximum expiry duration + */ + function MAX_EXPIRY() external pure returns (uint64); + + /** + * @dev Returns the 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 (uint256); + + /** + * @dev The expiry duration of a domain after transferring to bidder. + */ + function DOMAIN_EXPIRY_DURATION() external pure returns (uint64); + + /** + * @dev Claims domain names for auction. + * + * Requirements: + * - The method caller must be contract operator. + * + * @param labels The domain names. Eg, ['foo'] for 'foo.ron' + * @return ids The id corresponding for namehash of domain names. + */ + function bulkRegister(string[] calldata labels) external returns (uint256[] memory ids); + + /** + * @dev Checks whether a domain name is currently reserved for auction or not. + * @param id The namehash id of domain name. Eg, namehash('foo.ron') for 'foo.ron' + */ + function reserved(uint256 id) external view returns (bool); + + /** + * @dev Creates a new auction to sale with a specific time period. + * + * Requirements: + * - The method caller must be admin. + * + * Emits an event {AuctionEventSet}. + * + * @return auctionId The auction id + * @notice Please use the method `setAuctionNames` to list all the reserved names. + */ + function createAuctionEvent(EventRange calldata range) external returns (bytes32 auctionId); + + /** + * @dev Updates the auction details. + * + * Requirements: + * - The method caller must be admin. + * + * Emits an event {AuctionEventSet}. + */ + function setAuctionEvent(bytes32 auctionId, EventRange calldata range) external; + + /** + * @dev Returns the event range of an auction. + */ + function getAuctionEvent(bytes32 auctionId) external view returns (EventRange memory); + + /** + * @dev Lists reserved names to sale in a specified auction. + * + * Requirements: + * - The method caller must be contract operator. + * - Array length are matched and larger than 0. + * - Only allow to set when the domain is: + * + Not in any auction. + * + Or, in the current auction. + * + Or, this name is not bided. + * + * Emits an event {LabelsListed}. + * + * Note: If the name is already listed, this method replaces with a new input value. + * + * @param ids The namehashes id of domain names. Eg, namehash('foo.ron') for 'foo.ron' + */ + function listNamesForAuction(bytes32 auctionId, uint256[] calldata ids, uint256[] calldata startingPrices) external; + + /** + * @dev Places a bid for a domain name. + * + * Requirements: + * - The name is listed, or the auction is happening. + * - The msg.value is larger than the current bid price or the auction starting price. + * + * Emits an event {BidPlaced}. + * + * @param id The namehash id of domain name. Eg, namehash('foo.ron') for 'foo.ron' + */ + function placeBid(uint256 id) external payable; + + /** + * @dev Returns the highest bid and address of the bidder. + * @param id The namehash id of domain name. Eg, namehash('foo.ron') for 'foo.ron' + */ + function getAuction(uint256 id) external view returns (DomainAuction memory, uint256 beatPrice); + + /** + * @dev Bulk claims the bid name. + * + * Requirements: + * - Must be called after ended time. + * - The method caller can be anyone. + * + * @param ids The namehash id of domain name. Eg, namehash('foo.ron') for 'foo.ron' + */ + function bulkClaimBidNames(uint256[] calldata ids) external returns (bool[] memory claimeds); + + /** + * @dev Returns the treasury. + */ + function getTreasury() external view returns (address); + + /** + * @dev Returns the gap ratio between 2 bids with the starting price. Value in range [0;100_00] is 0%-100%. + */ + function getBidGapRatio() external view returns (uint256); + + /** + * @dev Sets the treasury. + * + * Requirements: + * - The method caller must be admin + * + * Emits an event {TreasuryUpdated}. + */ + function setTreasury(address payable) external; + + /** + * @dev Sets commission ratio. Value in range [0;100_00] is 0%-100%. + * + * Requirements: + * - The method caller must be admin + * + * Emits an event {BidGapRatioUpdated}. + */ + function setBidGapRatio(uint256) external; + + /** + * @dev Returns RNSUnified contract. + */ + function getRNSUnified() external view returns (INSUnified); +} diff --git a/src/interfaces/INSDomainPrice.sol b/src/interfaces/INSDomainPrice.sol new file mode 100644 index 00000000..b543c1d1 --- /dev/null +++ b/src/interfaces/INSDomainPrice.sol @@ -0,0 +1,205 @@ +//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; + } + + /// @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 (uint256 usdPrice, uint256 ronPrice); + + /** + * @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/LibEventRange.sol b/src/libraries/LibEventRange.sol new file mode 100644 index 00000000..00586d73 --- /dev/null +++ b/src/libraries/LibEventRange.sol @@ -0,0 +1,37 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +struct EventRange { + uint256 startedAt; + uint256 endedAt; +} + +library LibEventRange { + /** + * @dev Checks whether the event range is valid. + */ + function valid(EventRange calldata range) internal pure returns (bool) { + return range.startedAt <= range.endedAt; + } + + /** + * @dev Returns whether the current range is not yet started. + */ + function isNotYetStarted(EventRange memory range) internal view returns (bool) { + return block.timestamp < range.startedAt; + } + + /** + * @dev Returns whether the current range is ended or not. + */ + function isEnded(EventRange memory range) internal view returns (bool) { + return range.endedAt <= block.timestamp; + } + + /** + * @dev Returns whether the current block is in period. + */ + function isInPeriod(EventRange memory range) internal view returns (bool) { + return range.startedAt <= block.timestamp && block.timestamp < range.endedAt; + } +} diff --git a/src/libraries/LibRNSDomain.sol b/src/libraries/LibRNSDomain.sol new file mode 100644 index 00000000..5ced21a7 --- /dev/null +++ b/src/libraries/LibRNSDomain.sol @@ -0,0 +1,55 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +library LibRNSDomain { + /// @dev Value equals to namehash('ron') + uint256 internal constant RON_ID = 0xba69923fa107dbf5a25a073a10b7c9216ae39fbadc95dc891d460d9ae315d688; + + /** + * @dev Calculate the corresponding id given parentId and label. + */ + function toId(uint256 parentId, string memory label) internal pure returns (uint256 id) { + assembly ("memory-safe") { + mstore(0x0, parentId) + mstore(0x20, keccak256(add(label, 32), mload(label))) + id := keccak256(0x0, 64) + } + } + + function hashLabel(string memory label) internal pure returns (bytes32 hashed) { + assembly ("memory-safe") { + 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 }); + } +} From 113a81e7a4048e1f78096b6ef85c413866ee2cc8 Mon Sep 17 00:00:00 2001 From: Tu Do <18521578@gm.uit.edu.vn> Date: Thu, 12 Oct 2023 17:15:38 +0700 Subject: [PATCH 2/4] chore: resolve merge conflict 'feature/domain-price' into release-testnet/v0.2.0 (#27) * feat: implement Auction contract for RNS (#18) feat: add RNSAuction * fix: resolve conflict * forge install: pyth-sdk-solidity v2.2.0 * fix: add remapping for @pythnetwork --- .gitmodules | 6 + lib/buffer | 1 + lib/ens-contracts | 1 + remappings.txt | 2 + src/RNSAuction.sol | 334 ++++++++++++++++++ src/RNSUnified.sol | 5 + src/extensions/Multicallable.sol | 52 +++ src/interfaces/IMulticallable.sol | 37 ++ src/interfaces/INSUnified.sol | 5 + src/interfaces/IReverseRegistrar.sol | 2 +- src/interfaces/resolvers/IABIResolver.sol | 38 ++ src/interfaces/resolvers/IAddressResolver.sol | 28 ++ .../resolvers/IContentHashResolver.sol | 28 ++ .../resolvers/IDNSRecordResolver.sol | 41 +++ src/interfaces/resolvers/IDNSZoneResolver.sol | 28 ++ .../resolvers/IInterfaceResolver.sol | 34 ++ .../resolvers/IPublicKeyResolver.sol | 37 ++ src/interfaces/resolvers/IPublicResolver.sol | 60 ++++ src/interfaces/resolvers/ITextResolver.sol | 30 ++ src/interfaces/resolvers/IVersionResolver.sol | 25 ++ src/libraries/ErrorHandler.sol | 22 ++ src/libraries/transfers/RONTransferHelper.sol | 31 ++ src/resolvers/ABIResolvable.sol | 45 +++ src/resolvers/AddressResolvable.sol | 36 ++ src/resolvers/BaseVersion.sol | 36 ++ src/resolvers/ContentHashResolvable.sol | 36 ++ src/resolvers/DNSResolvable.sol | 141 ++++++++ src/resolvers/InterfaceResolvable.sol | 68 ++++ src/resolvers/NameResolvable.sol | 35 ++ src/resolvers/PublicKeyResolvable.sol | 36 ++ src/resolvers/PublicResolver.sol | 190 ++++++++++ src/resolvers/TextResolvable.sol | 34 ++ 32 files changed, 1503 insertions(+), 1 deletion(-) create mode 160000 lib/buffer create mode 160000 lib/ens-contracts create mode 100644 src/RNSAuction.sol create mode 100644 src/extensions/Multicallable.sol create mode 100644 src/interfaces/IMulticallable.sol create mode 100644 src/interfaces/resolvers/IABIResolver.sol create mode 100644 src/interfaces/resolvers/IAddressResolver.sol create mode 100644 src/interfaces/resolvers/IContentHashResolver.sol create mode 100644 src/interfaces/resolvers/IDNSRecordResolver.sol create mode 100644 src/interfaces/resolvers/IDNSZoneResolver.sol create mode 100644 src/interfaces/resolvers/IInterfaceResolver.sol create mode 100644 src/interfaces/resolvers/IPublicKeyResolver.sol create mode 100644 src/interfaces/resolvers/IPublicResolver.sol create mode 100644 src/interfaces/resolvers/ITextResolver.sol create mode 100644 src/interfaces/resolvers/IVersionResolver.sol create mode 100644 src/libraries/ErrorHandler.sol create mode 100644 src/libraries/transfers/RONTransferHelper.sol create mode 100644 src/resolvers/ABIResolvable.sol create mode 100644 src/resolvers/AddressResolvable.sol create mode 100644 src/resolvers/BaseVersion.sol create mode 100644 src/resolvers/ContentHashResolvable.sol create mode 100644 src/resolvers/DNSResolvable.sol create mode 100644 src/resolvers/InterfaceResolvable.sol create mode 100644 src/resolvers/NameResolvable.sol create mode 100644 src/resolvers/PublicKeyResolvable.sol create mode 100644 src/resolvers/PublicResolver.sol create mode 100644 src/resolvers/TextResolvable.sol diff --git a/.gitmodules b/.gitmodules index 15220f8c..853782e5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,6 +7,12 @@ [submodule "lib/contract-template"] path = lib/contract-template url = https://github.com/axieinfinity/contract-template +[submodule "lib/ens-contracts"] + path = lib/ens-contracts + url = https://github.com/ensdomains/ens-contracts +[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/buffer b/lib/buffer new file mode 160000 index 00000000..688aa09e --- /dev/null +++ b/lib/buffer @@ -0,0 +1 @@ +Subproject commit 688aa09e9ad241a94609e6af539e65f229912b16 diff --git a/lib/ens-contracts b/lib/ens-contracts new file mode 160000 index 00000000..0c75ba23 --- /dev/null +++ b/lib/ens-contracts @@ -0,0 +1 @@ +Subproject commit 0c75ba23fae76165d51c9c80d76d22261e06179d diff --git a/remappings.txt b/remappings.txt index 20df439b..5c3ceb13 100644 --- a/remappings.txt +++ b/remappings.txt @@ -3,4 +3,6 @@ ds-test/=lib/forge-std/lib/ds-test/src/ 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/ @pythnetwork/=lib/pyth-sdk-solidity/ \ No newline at end of file diff --git a/src/RNSAuction.sol b/src/RNSAuction.sol new file mode 100644 index 00000000..f95b7a65 --- /dev/null +++ b/src/RNSAuction.sol @@ -0,0 +1,334 @@ +// 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 { INSUnified, INSAuction } from "./interfaces/INSAuction.sol"; +import { LibSafeRange } from "./libraries/math/LibSafeRange.sol"; +import { LibRNSDomain } from "./libraries/LibRNSDomain.sol"; +import { LibEventRange, EventRange } from "./libraries/LibEventRange.sol"; +import { RONTransferHelper } from "./libraries/transfers/RONTransferHelper.sol"; + +contract RNSAuction is Initializable, AccessControlEnumerable, INSAuction { + using LibSafeRange for uint256; + using LibEventRange for EventRange; + + /// @inheritdoc INSAuction + uint64 public constant MAX_EXPIRY = type(uint64).max; + /// @inheritdoc INSAuction + uint256 public constant MAX_PERCENTAGE = 100_00; + /// @inheritdoc INSAuction + uint64 public constant DOMAIN_EXPIRY_DURATION = 365 days; + /// @inheritdoc INSAuction + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + + /// @dev Gap for upgradeability. + uint256[50] private ____gap; + + /// @dev The RNSUnified contract. + INSUnified internal _rnsUnified; + /// @dev Mapping from auction Id => event range + mapping(bytes32 auctionId => EventRange) internal _auctionRange; + /// @dev Mapping from id of domain names => auction detail. + mapping(uint256 id => DomainAuction) internal _domainAuction; + + /// @dev The treasury. + address payable internal _treasury; + /// @dev The gap ratio between 2 bids with the starting price. + uint256 internal _bidGapRatio; + + modifier whenNotStarted(bytes32 auctionId) { + _requireNotStarted(auctionId); + _; + } + + modifier onlyValidEventRange(EventRange calldata range) { + _requireValidEventRange(range); + _; + } + + constructor() payable { + _disableInitializers(); + } + + function initialize( + address admin, + address[] calldata operators, + INSUnified rnsUnified, + address payable treasury, + uint256 bidGapRatio + ) external initializer { + _setTreasury(treasury); + _setBidGapRatio(bidGapRatio); + _setupRole(DEFAULT_ADMIN_ROLE, admin); + + uint256 length = operators.length; + bytes32 operatorRole = OPERATOR_ROLE; + + for (uint256 i; i < length;) { + _setupRole(operatorRole, operators[i]); + + unchecked { + ++i; + } + } + + _rnsUnified = rnsUnified; + } + + /** + * @inheritdoc INSAuction + */ + function bulkRegister(string[] calldata labels) external onlyRole(OPERATOR_ROLE) returns (uint256[] memory ids) { + uint256 length = labels.length; + if (length == 0) revert InvalidArrayLength(); + ids = new uint256[](length); + INSUnified rnsUnified = _rnsUnified; + uint256 parentId = LibRNSDomain.RON_ID; + uint64 domainExpiryDuration = DOMAIN_EXPIRY_DURATION; + + for (uint256 i; i < length;) { + (, ids[i]) = rnsUnified.mint(parentId, labels[i], address(0x0), address(this), domainExpiryDuration); + + unchecked { + ++i; + } + } + } + + /** + * @inheritdoc INSAuction + */ + function reserved(uint256 id) public view returns (bool) { + return _rnsUnified.ownerOf(id) == address(this); + } + + /** + * @inheritdoc INSAuction + */ + function createAuctionEvent(EventRange calldata range) + external + onlyRole(DEFAULT_ADMIN_ROLE) + onlyValidEventRange(range) + returns (bytes32 auctionId) + { + auctionId = keccak256(abi.encode(_msgSender(), range)); + _auctionRange[auctionId] = range; + emit AuctionEventSet(auctionId, range); + } + + /** + * @inheritdoc INSAuction + */ + function setAuctionEvent(bytes32 auctionId, EventRange calldata range) + external + onlyRole(DEFAULT_ADMIN_ROLE) + onlyValidEventRange(range) + whenNotStarted(auctionId) + { + _auctionRange[auctionId] = range; + emit AuctionEventSet(auctionId, range); + } + + /** + * @inheritdoc INSAuction + */ + function getAuctionEvent(bytes32 auctionId) public view returns (EventRange memory) { + return _auctionRange[auctionId]; + } + + /** + * @inheritdoc INSAuction + */ + function listNamesForAuction(bytes32 auctionId, uint256[] calldata ids, uint256[] calldata startingPrices) + external + onlyRole(OPERATOR_ROLE) + whenNotStarted(auctionId) + { + uint256 length = ids.length; + if (length == 0 || length != startingPrices.length) revert InvalidArrayLength(); + uint256 id; + bytes32 mAuctionId; + DomainAuction storage sAuction; + + for (uint256 i; i < length;) { + id = ids[i]; + if (!reserved(id)) revert NameNotReserved(); + + sAuction = _domainAuction[id]; + mAuctionId = sAuction.auctionId; + if (!(mAuctionId == 0 || mAuctionId == auctionId || sAuction.bid.timestamp == 0)) { + revert AlreadyBidding(); + } + + sAuction.auctionId = auctionId; + sAuction.startingPrice = startingPrices[i]; + + unchecked { + ++i; + } + } + + emit LabelsListed(auctionId, ids, startingPrices); + } + + /** + * @inheritdoc INSAuction + */ + function placeBid(uint256 id) external payable { + DomainAuction memory auction = _domainAuction[id]; + EventRange memory range = _auctionRange[auction.auctionId]; + uint256 beatPrice = _getBeatPrice(auction, range); + + if (!range.isInPeriod()) revert QueryIsNotInPeriod(); + if (msg.value < beatPrice) revert InsufficientAmount(); + address payable bidder = payable(_msgSender()); + // check whether the bidder can receive RON + if (!RONTransferHelper.send(bidder, 0)) revert BidderCannotReceiveRON(); + address payable prvBidder = auction.bid.bidder; + uint256 prvPrice = auction.bid.price; + + Bid storage sBid = _domainAuction[id].bid; + sBid.price = msg.value; + sBid.bidder = bidder; + sBid.timestamp = block.timestamp; + emit BidPlaced(auction.auctionId, id, msg.value, bidder, prvPrice, prvBidder); + + // refund for previous bidder + if (prvPrice != 0) RONTransferHelper.safeTransfer(prvBidder, prvPrice); + } + + /** + * @inheritdoc INSAuction + */ + function bulkClaimBidNames(uint256[] calldata ids) external returns (bool[] memory claimeds) { + uint256 id; + uint256 accumulatedRON; + EventRange memory range; + DomainAuction memory auction; + uint256 length = ids.length; + claimeds = new bool[](length); + INSUnified rnsUnified = _rnsUnified; + uint64 expiry = uint64(block.timestamp.addWithUpperbound(DOMAIN_EXPIRY_DURATION, MAX_EXPIRY)); + + for (uint256 i; i < length;) { + id = ids[i]; + auction = _domainAuction[id]; + range = _auctionRange[auction.auctionId]; + + if (!auction.bid.claimed) { + if (!range.isEnded()) revert NotYetEnded(); + if (auction.bid.timestamp == 0) revert NoOneBidded(); + + accumulatedRON += auction.bid.price; + rnsUnified.setExpiry(id, expiry); + rnsUnified.transferFrom(address(this), auction.bid.bidder, id); + + _domainAuction[id].bid.claimed = claimeds[i] = true; + } + + unchecked { + ++i; + } + } + + RONTransferHelper.safeTransfer(_treasury, accumulatedRON); + } + + /** + * @inheritdoc INSAuction + */ + function getRNSUnified() external view returns (INSUnified) { + return _rnsUnified; + } + + /** + * @inheritdoc INSAuction + */ + function getTreasury() external view returns (address) { + return _treasury; + } + + /** + * @inheritdoc INSAuction + */ + function getBidGapRatio() external view returns (uint256) { + return _bidGapRatio; + } + + /** + * @inheritdoc INSAuction + */ + function setTreasury(address payable addr) external onlyRole(DEFAULT_ADMIN_ROLE) { + _setTreasury(addr); + } + + /** + * @inheritdoc INSAuction + */ + + function setBidGapRatio(uint256 ratio) external onlyRole(DEFAULT_ADMIN_ROLE) { + _setBidGapRatio(ratio); + } + + /** + * @inheritdoc INSAuction + */ + function getAuction(uint256 id) public view returns (DomainAuction memory auction, uint256 beatPrice) { + auction = _domainAuction[id]; + EventRange memory range = getAuctionEvent(auction.auctionId); + beatPrice = _getBeatPrice(auction, range); + } + + /** + * @dev Helper method to set treasury. + * + * Emits an event {TreasuryUpdated}. + */ + function _setTreasury(address payable addr) internal { + if (addr == address(0)) revert NullAssignment(); + _treasury = addr; + emit TreasuryUpdated(addr); + } + + /** + * @dev Helper method to set bid gap ratio. + * + * Emits an event {BidGapRatioUpdated}. + */ + function _setBidGapRatio(uint256 ratio) internal { + if (ratio > MAX_PERCENTAGE) revert RatioIsTooLarge(); + _bidGapRatio = ratio; + emit BidGapRatioUpdated(ratio); + } + + /** + * @dev Helper method to get beat price. + */ + function _getBeatPrice(DomainAuction memory auction, EventRange memory range) + internal + view + returns (uint256 beatPrice) + { + beatPrice = Math.max(auction.startingPrice, auction.bid.price); + // Beats price increases if domain is already bided and the event is not yet ended. + if (auction.bid.price != 0 && !range.isEnded()) { + beatPrice += Math.mulDiv(auction.startingPrice, _bidGapRatio, MAX_PERCENTAGE); + } + } + + /** + * @dev Helper method to ensure event range is valid. + */ + function _requireValidEventRange(EventRange calldata range) internal view { + if (!(range.valid() && range.isNotYetStarted())) revert InvalidEventRange(); + } + + /** + * @dev Helper method to ensure the auction is not yet started or not created. + */ + function _requireNotStarted(bytes32 auctionId) internal view { + if (!_auctionRange[auctionId].isNotYetStarted()) revert EventIsNotCreatedOrAlreadyStarted(); + } +} diff --git a/src/RNSUnified.sol b/src/RNSUnified.sol index 134c1cba..65b60bd9 100644 --- a/src/RNSUnified.sol +++ b/src/RNSUnified.sol @@ -58,6 +58,11 @@ contract RNSUnified is Initializable, RNSToken { emit RecordUpdated(0x0, ModifyingField.Expiry.indicator(), record); } + /// @inheritdoc INSUnified + function namehash(string memory) external pure returns (bytes32 node) { + revert("TODO"); + } + /// @inheritdoc INSUnified function available(uint256 id) public view returns (bool) { return block.timestamp > LibSafeRange.add(_expiry(id), _gracePeriod); diff --git a/src/extensions/Multicallable.sol b/src/extensions/Multicallable.sol new file mode 100644 index 00000000..fc6f52b6 --- /dev/null +++ b/src/extensions/Multicallable.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import { IMulticallable } from "@rns-contracts/interfaces/IMulticallable.sol"; +import { ErrorHandler } from "@rns-contracts/libraries/ErrorHandler.sol"; + +abstract contract Multicallable is ERC165, IMulticallable { + using ErrorHandler for bool; + + /** + * @dev Override {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IMulticallable).interfaceId || super.supportsInterface(interfaceID); + } + + /** + * @inheritdoc IMulticallable + */ + function multicall(bytes[] calldata data) public override returns (bytes[] memory results) { + return _tryMulticall(true, data); + } + + /** + * @inheritdoc IMulticallable + */ + function tryMulticall(bool requireSuccess, bytes[] calldata data) public override returns (bytes[] memory results) { + return _tryMulticall(requireSuccess, data); + } + + /** + * @dev See {IMulticallable-tryMulticall}. + */ + function _tryMulticall(bool requireSuccess, bytes[] calldata data) internal returns (bytes[] memory results) { + uint256 length = data.length; + results = new bytes[](length); + + bool success; + bytes memory result; + + for (uint256 i; i < length;) { + (success, result) = address(this).delegatecall(data[i]); + if (requireSuccess) success.handleRevert(result); + results[i] = result; + + unchecked { + ++i; + } + } + } +} diff --git a/src/interfaces/IMulticallable.sol b/src/interfaces/IMulticallable.sol new file mode 100644 index 00000000..5e536433 --- /dev/null +++ b/src/interfaces/IMulticallable.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +/** + * @notice To multi-call to a specified contract which has multicall interface: + * + * ```solidity + * interface IMock is IMulticallable { + * function foo() external; + * function bar() external; + * } + * + * bytes[] memory calldatas = new bytes[](2); + * calldatas[0] = abi.encodeCall(IMock.foo,()); + * calldatas[1] = abi.encodeCall(IMock.bar,()); + * IMock(target).multicall(calldatas); + * ``` + */ +interface IMulticallable { + /** + * @dev Executes bulk action to the original contract. + * Reverts if there is a single call failed. + * + * @param data The calldata to original contract. + * + */ + function multicall(bytes[] calldata data) external returns (bytes[] memory results); + + /** + * @dev Executes bulk action to the original contract. + * + * @param requireSuccess Flag to indicating whether the contract reverts if there is a single call failed. + * @param data The calldata to original contract. + * + */ + function tryMulticall(bool requireSuccess, bytes[] calldata data) external returns (bytes[] memory results); +} diff --git a/src/interfaces/INSUnified.sol b/src/interfaces/INSUnified.sol index 0863335f..9742ebab 100644 --- a/src/interfaces/INSUnified.sol +++ b/src/interfaces/INSUnified.sol @@ -104,6 +104,11 @@ interface INSUnified is IAccessControlEnumerable, IERC721Metadata { */ function MAX_EXPIRY() external pure returns (uint64); + /** + * @dev Returns the name hash output of a domain. + */ + function namehash(string memory domain) external pure returns (bytes32 node); + /** * @dev Returns true if the specified name is available for registration. * Note: Only available after passing the grace period. diff --git a/src/interfaces/IReverseRegistrar.sol b/src/interfaces/IReverseRegistrar.sol index f3fe2d1e..c40e25b3 100644 --- a/src/interfaces/IReverseRegistrar.sol +++ b/src/interfaces/IReverseRegistrar.sol @@ -1,4 +1,4 @@ -// SPDX-LicINSe-Identifier: UNLICINSED +// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; diff --git a/src/interfaces/resolvers/IABIResolver.sol b/src/interfaces/resolvers/IABIResolver.sol new file mode 100644 index 00000000..678ef5f1 --- /dev/null +++ b/src/interfaces/resolvers/IABIResolver.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +interface IABIResolver { + /// Thrown when the input content type is invalid. + error InvalidContentType(); + + /// @dev Emitted when the ABI is changed. + event ABIChanged(bytes32 indexed node, uint256 indexed contentType); + + /** + * @dev Sets the ABI associated with an INS node. Nodes may have one ABI of each content type. To remove an ABI, set it + * to the empty string. + * + * Requirements: + * - The method caller must be authorized to change user fields of RNS Token `node`. See indicator + * {ModifyingIndicator.USER_FIELDS_INDICATOR}. + * - The content type must be powers of 2. + * + * Emitted an event {ABIChanged}. + * + * @param node The node to update. + * @param contentType The content type of the ABI + * @param data The ABI data. + */ + function setABI(bytes32 node, uint256 contentType, bytes calldata data) external; + + /** + * @dev Returns the ABI associated with an INS node. + * Defined in EIP-205, see more at https://eips.ethereum.org/EIPS/eip-205 + * + * @param node The INS node to query + * @param contentTypes A bitwise OR of the ABI formats accepted by the caller. + * @return contentType The content type of the return value + * @return data The ABI data + */ + function ABI(bytes32 node, uint256 contentTypes) external view returns (uint256 contentType, bytes memory data); +} diff --git a/src/interfaces/resolvers/IAddressResolver.sol b/src/interfaces/resolvers/IAddressResolver.sol new file mode 100644 index 00000000..84c986fa --- /dev/null +++ b/src/interfaces/resolvers/IAddressResolver.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +interface IAddressResolver { + /// @dev Emitted when an address of a node is changed. + event AddrChanged(bytes32 indexed node, address addr); + + /** + * @dev Sets the address associated with an INS node. + * + * Requirement: + * - The method caller must be authorized to change user fields of RNS Token `node`. See indicator + * {ModifyingIndicator.USER_FIELDS_INDICATOR}. + * + * Emits an event {AddrChanged}. + * + * @param node The node to update. + * @param addr The address to set. + */ + function setAddr(bytes32 node, address addr) external; + + /** + * @dev Returns the address associated with an INS node. + * @param node The INS node to query. + * @return The associated address. + */ + function addr(bytes32 node) external view returns (address payable); +} diff --git a/src/interfaces/resolvers/IContentHashResolver.sol b/src/interfaces/resolvers/IContentHashResolver.sol new file mode 100644 index 00000000..7db60259 --- /dev/null +++ b/src/interfaces/resolvers/IContentHashResolver.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IContentHashResolver { + /// @dev Emitted when the content hash of a node is changed. + event ContentHashChanged(bytes32 indexed node, bytes hash); + + /** + * @dev Sets the content hash associated with an INS node. + * + * Requirements: + * - The method caller must be authorized to change user fields of RNS Token `node`. See indicator + * {ModifyingIndicator.USER_FIELDS_INDICATOR}. + * + * Emits an event {ContentHashChanged}. + * + * @param node The node to update. + * @param hash The content hash to set + */ + function setContentHash(bytes32 node, bytes calldata hash) external; + + /** + * @dev Returns the content hash associated with an INS node. + * @param node The INS node to query. + * @return The associated content hash. + */ + function contentHash(bytes32 node) external view returns (bytes memory); +} diff --git a/src/interfaces/resolvers/IDNSRecordResolver.sol b/src/interfaces/resolvers/IDNSRecordResolver.sol new file mode 100644 index 00000000..97e5434e --- /dev/null +++ b/src/interfaces/resolvers/IDNSRecordResolver.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.4; + +interface IDNSRecordResolver { + /// @dev Emitted whenever a given node/name/resource's RRSET is updated. + event DNSRecordChanged(bytes32 indexed node, bytes name, uint16 resource, bytes record); + /// @dev Emitted whenever a given node/name/resource's RRSET is deleted. + event DNSRecordDeleted(bytes32 indexed node, bytes name, uint16 resource); + + /** + * @dev Set one or more DNS records. Records are supplied in wire-format. Records with the same node/name/resource + * must be supplied one after the other to ensure the data is updated correctly. For example, if the data was + * supplied: + * a.example.com IN A 1.2.3.4 + * a.example.com IN A 5.6.7.8 + * www.example.com IN CNAME a.example.com. + * then this would store the two A records for a.example.com correctly as a single RRSET, however if the data was + * supplied: + * a.example.com IN A 1.2.3.4 + * www.example.com IN CNAME a.example.com. + * a.example.com IN A 5.6.7.8 + * then this would store the first A record, the CNAME, then the second A record which would overwrite the first. + * + * Requirements: + * - The method caller must be authorized to change user fields of RNS Token `node`. See indicator + * {ModifyingIndicator.USER_FIELDS_INDICATOR}. + * + * @param node the namehash of the node for which to set the records + * @param data the DNS wire format records to set + */ + function setDNSRecords(bytes32 node, bytes calldata data) external; + + /** + * @dev Obtain a DNS record. + * @param node the namehash of the node for which to fetch the record + * @param name the keccak-256 hash of the fully-qualified name for which to fetch the record + * @param resource the ID of the resource as per https://en.wikipedia.org/wiki/List_of_DNS_record_types + * @return the DNS record in wire format if present, otherwise empty + */ + function dnsRecord(bytes32 node, bytes32 name, uint16 resource) external view returns (bytes memory); +} diff --git a/src/interfaces/resolvers/IDNSZoneResolver.sol b/src/interfaces/resolvers/IDNSZoneResolver.sol new file mode 100644 index 00000000..ea74e062 --- /dev/null +++ b/src/interfaces/resolvers/IDNSZoneResolver.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.4; + +interface IDNSZoneResolver { + /// @dev Emitted whenever a given node's zone hash is updated. + event DNSZonehashChanged(bytes32 indexed node, bytes lastzonehash, bytes zonehash); + + /** + * @dev Sets the hash for the zone. + * + * Requirements: + * - The method caller must be authorized to change user fields of RNS Token `node`. See indicator + * {ModifyingIndicator.USER_FIELDS_INDICATOR}. + * + * Emits an event {DNSZonehashChanged}. + * + * @param node The node to update. + * @param hash The zonehash to set + */ + function setZonehash(bytes32 node, bytes calldata hash) external; + + /** + * @dev Obtains the hash for the zone. + * @param node The INS node to query. + * @return The associated contenthash. + */ + function zonehash(bytes32 node) external view returns (bytes memory); +} diff --git a/src/interfaces/resolvers/IInterfaceResolver.sol b/src/interfaces/resolvers/IInterfaceResolver.sol new file mode 100644 index 00000000..e6f6018a --- /dev/null +++ b/src/interfaces/resolvers/IInterfaceResolver.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IInterfaceResolver { + /// @dev Emitted when the interface of node is changed. + event InterfaceChanged(bytes32 indexed node, bytes4 indexed interfaceID, address implementer); + + /** + * @dev Sets an interface associated with a name. + * Setting the address to 0 restores the default behaviour of querying the contract at `addr()` for interface support. + * + * Requirements: + * - The method caller must be authorized to change user fields of RNS Token `node`. See indicator + * {ModifyingIndicator.USER_FIELDS_INDICATOR}. + * + * @param node The node to update. + * @param interfaceID The EIP 165 interface ID. + * @param implementer The address of a contract that implements this interface for this node. + */ + function setInterface(bytes32 node, bytes4 interfaceID, address implementer) external; + + /** + * @dev Returns the address of a contract that implements the specified interface for this name. + * + * If an implementer has not been set for this interfaceID and name, the resolver will query the contract at `addr()`. + * If `addr()` is set, a contract exists at that address, and that contract implements EIP165 and returns `true` for + * the specified interfaceID, its address will be returned. + * + * @param node The INS node to query. + * @param interfaceID The EIP 165 interface ID to check for. + * @return The address that implements this interface, or 0 if the interface is unsupported. + */ + function interfaceImplementer(bytes32 node, bytes4 interfaceID) external view returns (address); +} diff --git a/src/interfaces/resolvers/IPublicKeyResolver.sol b/src/interfaces/resolvers/IPublicKeyResolver.sol new file mode 100644 index 00000000..760aa454 --- /dev/null +++ b/src/interfaces/resolvers/IPublicKeyResolver.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IPublicKeyResolver { + struct PublicKey { + bytes32 x; + bytes32 y; + } + + /// @dev Emitted when a node public key is changed. + event PubkeyChanged(bytes32 indexed node, bytes32 x, bytes32 y); + + /** + * @dev Sets the SECP256k1 public key associated with an INS node. + * + * Requirements: + * - The method caller must be authorized to change user fields of RNS Token `node`. See indicator + * {ModifyingIndicator.USER_FIELDS_INDICATOR}. + * + * Emits an event {PubkeyChanged}. + * + * @param node The INS node to query + * @param x the X coordinate of the curve point for the public key. + * @param y the Y coordinate of the curve point for the public key. + */ + function setPubkey(bytes32 node, bytes32 x, bytes32 y) external; + + /** + * @dev Returns the SECP256k1 public key associated with an INS node. + * Defined in EIP 619. + * + * @param node The INS node to query + * @return x The X coordinate of the curve point for the public key. + * @return y The Y coordinate of the curve point for the public key. + */ + function pubkey(bytes32 node) external view returns (bytes32 x, bytes32 y); +} diff --git a/src/interfaces/resolvers/IPublicResolver.sol b/src/interfaces/resolvers/IPublicResolver.sol new file mode 100644 index 00000000..a2e0d67f --- /dev/null +++ b/src/interfaces/resolvers/IPublicResolver.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { INSUnified } from "@rns-contracts/interfaces/INSUnified.sol"; +import { IReverseRegistrar } from "@rns-contracts/interfaces/IReverseRegistrar.sol"; +import { IABIResolver } from "./IABIResolver.sol"; +import { IAddressResolver } from "./IAddressResolver.sol"; +import { IContentHashResolver } from "./IContentHashResolver.sol"; +import { IDNSRecordResolver } from "./IDNSRecordResolver.sol"; +import { IDNSZoneResolver } from "./IDNSZoneResolver.sol"; +import { IInterfaceResolver } from "./IInterfaceResolver.sol"; +import { INameResolver } from "./INameResolver.sol"; +import { IPublicKeyResolver } from "./IPublicKeyResolver.sol"; +import { ITextResolver } from "./ITextResolver.sol"; +import { IMulticallable } from "../IMulticallable.sol"; + +interface IPublicResolver is + IABIResolver, + IAddressResolver, + IContentHashResolver, + IDNSRecordResolver, + IDNSZoneResolver, + IInterfaceResolver, + INameResolver, + IPublicKeyResolver, + ITextResolver, + IMulticallable +{ + /// @dev See {IERC1155-ApprovalForAll}. Logged when an operator is added or removed. + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + /// @dev Logged when a delegate is approved or an approval is revoked. + event Approved(address owner, bytes32 indexed node, address indexed delegate, bool indexed approved); + + /** + * @dev Checks if an account is authorized to manage the resolution of a specific RNS node. + * @param node The RNS node. + * @param account The account address being checked for authorization. + * @return A boolean indicating whether the account is authorized. + */ + function isAuthorized(bytes32 node, address account) external view returns (bool); + + /** + * @dev Retrieves the RNSUnified associated with this resolver. + */ + function getRNSUnified() external view returns (INSUnified); + + /** + * @dev Retrieves the reverse registrar associated with this resolver. + */ + function getReverseRegistrar() external view returns (IReverseRegistrar); + + /** + * @dev This function provides an extra security check when called from privileged contracts (such as + * RONRegistrarController) that can set records on behalf of the node owners. + * + * Reverts if the node is not null but calldata is mismatched. + */ + function multicallWithNodeCheck(bytes32 node, bytes[] calldata data) external returns (bytes[] memory results); +} diff --git a/src/interfaces/resolvers/ITextResolver.sol b/src/interfaces/resolvers/ITextResolver.sol new file mode 100644 index 00000000..1408b4e4 --- /dev/null +++ b/src/interfaces/resolvers/ITextResolver.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface ITextResolver { + /// @dev Emitted when a node text is changed. + event TextChanged(bytes32 indexed node, string indexed indexedKey, string key, string value); + + /** + * @dev Sets the text data associated with an INS node and key. + * + * Requirements: + * - The method caller must be authorized to change user fields of RNS Token `node`. See indicator + * {ModifyingIndicator.USER_FIELDS_INDICATOR}. + * + * Emits an event {TextChanged}. + * + * @param node The node to update. + * @param key The key to set. + * @param value The text data value to set. + */ + function setText(bytes32 node, string calldata key, string calldata value) external; + + /** + * Returns the text data associated with an INS node and key. + * @param node The INS node to query. + * @param key The text data key to query. + * @return The associated text data. + */ + function text(bytes32 node, string calldata key) external view returns (string memory); +} diff --git a/src/interfaces/resolvers/IVersionResolver.sol b/src/interfaces/resolvers/IVersionResolver.sol new file mode 100644 index 00000000..b88a3ebb --- /dev/null +++ b/src/interfaces/resolvers/IVersionResolver.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IVersionResolver { + /// @dev Emitted when the version of a node is changed. + event VersionChanged(bytes32 indexed node, uint64 newVersion); + + /** + * @dev Increments the record version associated with an INS node. + * + * Requirements: + * - The method caller must be authorized to change user fields of RNS Token `node`. See indicator + * {ModifyingIndicator.USER_FIELDS_INDICATOR}. + * + * Emits an event {VersionChanged}. + * + * @param node The node to update. + */ + function clearRecords(bytes32 node) external; + + /** + * @dev Returns the latest version of a node. + */ + function recordVersions(bytes32 node) external view returns (uint64); +} diff --git a/src/libraries/ErrorHandler.sol b/src/libraries/ErrorHandler.sol new file mode 100644 index 00000000..5ea118d6 --- /dev/null +++ b/src/libraries/ErrorHandler.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +library ErrorHandler { + error ExternalCallFailed(); + + function handleRevert(bool status, bytes memory returnOrRevertData) internal pure { + assembly { + if iszero(status) { + let revertLength := mload(returnOrRevertData) + if iszero(iszero(revertLength)) { + // Start of revert data bytes. The 0x20 offset is always the same. + revert(add(returnOrRevertData, 0x20), revertLength) + } + + // revert ExternalCallFailed() + mstore(0x00, 0x350c20f1) + revert(0x1c, 0x04) + } + } + } +} diff --git a/src/libraries/transfers/RONTransferHelper.sol b/src/libraries/transfers/RONTransferHelper.sol new file mode 100644 index 00000000..2aa9bade --- /dev/null +++ b/src/libraries/transfers/RONTransferHelper.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; + +/** + * @title RONTransferHelper + */ +library RONTransferHelper { + using Strings for *; + + /** + * @dev Transfers RON and wraps result for the method caller to a recipient. + */ + function safeTransfer(address payable _to, uint256 _value) internal { + bool _success = send(_to, _value); + if (!_success) { + revert( + string.concat("TransferHelper: could not transfer RON to ", _to.toHexString(), " value ", _value.toHexString()) + ); + } + } + + /** + * @dev Returns whether the call was success. + * Note: this function should use with the `ReentrancyGuard`. + */ + function send(address payable _to, uint256 _value) internal returns (bool _success) { + (_success,) = _to.call{ value: _value }(new bytes(0)); + } +} diff --git a/src/resolvers/ABIResolvable.sol b/src/resolvers/ABIResolvable.sol new file mode 100644 index 00000000..5d9c6e05 --- /dev/null +++ b/src/resolvers/ABIResolvable.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "@rns-contracts/interfaces/resolvers/IABIResolver.sol"; +import "./BaseVersion.sol"; + +abstract contract ABIResolvable is IABIResolver, ERC165, BaseVersion { + /// @dev Gap for upgradeability. + uint256[50] private ____gap; + + /// @dev Mapping from version => node => content type => abi + mapping(uint64 version => mapping(bytes32 node => mapping(uint256 contentType => bytes abi))) internal _versionalAbi; + + /** + * @dev Override {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceID) public view virtual override(BaseVersion, ERC165) returns (bool) { + return interfaceID == type(IABIResolver).interfaceId || super.supportsInterface(interfaceID); + } + + /** + * @inheritdoc IABIResolver + */ + function ABI(bytes32 node, uint256 contentTypes) external view virtual override returns (uint256, bytes memory) { + mapping(uint256 contentType => bytes abi) storage abiSet = _versionalAbi[_recordVersion[node]][node]; + + for (uint256 contentType = 1; contentType <= contentTypes; contentType <<= 1) { + if ((contentType & contentTypes) != 0 && abiSet[contentType].length > 0) { + return (contentType, abiSet[contentType]); + } + } + + return (0, ""); + } + + /** + * @dev See {IABIResolver-setABI}. + */ + function _setABI(bytes32 node, uint256 contentType, bytes calldata data) internal { + if (((contentType - 1) & contentType) != 0) revert InvalidContentType(); + _versionalAbi[_recordVersion[node]][node][contentType] = data; + emit ABIChanged(node, contentType); + } +} diff --git a/src/resolvers/AddressResolvable.sol b/src/resolvers/AddressResolvable.sol new file mode 100644 index 00000000..65a46513 --- /dev/null +++ b/src/resolvers/AddressResolvable.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "@rns-contracts/interfaces/resolvers/IAddressResolver.sol"; +import "./BaseVersion.sol"; + +abstract contract AddressResolvable is IAddressResolver, ERC165, BaseVersion { + /// @dev Gap for upgradeability. + uint256[50] private ____gap; + + /// @dev Mapping from version => node => address + mapping(uint64 version => mapping(bytes32 node => address addr)) internal _versionAddress; + + /** + * @dev Override {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceID) public view virtual override(BaseVersion, ERC165) returns (bool) { + return interfaceID == type(IAddressResolver).interfaceId || super.supportsInterface(interfaceID); + } + + /** + * @inheritdoc IAddressResolver + */ + function addr(bytes32 node) public view virtual override returns (address payable) { + return payable(_versionAddress[_recordVersion[node]][node]); + } + + /** + * @dev See {IAddressResolver-setAddr}. + */ + function _setAddr(bytes32 node, address addr_) internal { + emit AddrChanged(node, addr_); + _versionAddress[_recordVersion[node]][node] = addr_; + } +} diff --git a/src/resolvers/BaseVersion.sol b/src/resolvers/BaseVersion.sol new file mode 100644 index 00000000..4d8ba90e --- /dev/null +++ b/src/resolvers/BaseVersion.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "@rns-contracts/interfaces/resolvers/IVersionResolver.sol"; + +abstract contract BaseVersion is IVersionResolver, ERC165 { + /// @dev Gap for upgradeability. + uint256[50] private ____gap; + + /// @dev Mapping from node => version + mapping(bytes32 node => uint64 version) internal _recordVersion; + + /** + * @dev Override {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IVersionResolver).interfaceId || super.supportsInterface(interfaceID); + } + + /** + * @inheritdoc IVersionResolver + */ + function recordVersions(bytes32 node) external view returns (uint64) { + return _recordVersion[node]; + } + + /** + * @dev See {IVersionResolver-clearRecords}. + */ + function _clearRecords(bytes32 node) internal { + unchecked { + emit VersionChanged(node, ++_recordVersion[node]); + } + } +} diff --git a/src/resolvers/ContentHashResolvable.sol b/src/resolvers/ContentHashResolvable.sol new file mode 100644 index 00000000..28a6f9be --- /dev/null +++ b/src/resolvers/ContentHashResolvable.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "@rns-contracts/interfaces/resolvers/IContentHashResolver.sol"; +import "./BaseVersion.sol"; + +abstract contract ContentHashResolvable is IContentHashResolver, ERC165, BaseVersion { + /// @dev Gap for upgradeability. + uint256[50] private ____gap; + + /// @dev Mapping from version => node => content hash + mapping(uint64 version => mapping(bytes32 node => bytes contentHash)) internal _versionContentHash; + + /** + * @dev Override {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceID) public view virtual override(BaseVersion, ERC165) returns (bool) { + return interfaceID == type(IContentHashResolver).interfaceId || super.supportsInterface(interfaceID); + } + + /** + * @inheritdoc IContentHashResolver + */ + function contentHash(bytes32 node) external view virtual override returns (bytes memory) { + return _versionContentHash[_recordVersion[node]][node]; + } + + /** + * @dev See {IContentHashResolver-setContentHash}. + */ + function _setContentHash(bytes32 node, bytes calldata hash) internal { + _versionContentHash[_recordVersion[node]][node] = hash; + emit ContentHashChanged(node, hash); + } +} diff --git a/src/resolvers/DNSResolvable.sol b/src/resolvers/DNSResolvable.sol new file mode 100644 index 00000000..a8d59ca1 --- /dev/null +++ b/src/resolvers/DNSResolvable.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "@ensdomains/ens-contracts/dnssec-oracle/RRUtils.sol"; +import "@rns-contracts/interfaces/resolvers/IDNSRecordResolver.sol"; +import "@rns-contracts/interfaces/resolvers/IDNSZoneResolver.sol"; +import "./BaseVersion.sol"; + +abstract contract DNSResolvable is IDNSRecordResolver, IDNSZoneResolver, ERC165, BaseVersion { + using RRUtils for *; + using BytesUtils for bytes; + + /// @dev Gap for upgradeability. + uint256[50] private ____gap; + + /// @dev The records themselves. Stored as binary RRSETs. + mapping( + uint64 version => mapping(bytes32 node => mapping(bytes32 nameHash => mapping(uint16 resource => bytes data))) + ) private _versionRecord; + + /// @dev Count of number of entries for a given name. Required for DNS resolvers when resolving wildcards. + mapping(uint64 version => mapping(bytes32 node => mapping(bytes32 nameHash => uint16 count))) private + _versionNameEntriesCount; + + /** + * @dev Zone hashes for the domains. A zone hash is an EIP-1577 content hash in binary format that should point to a + * resource containing a single zonefile. + */ + mapping(uint64 version => mapping(bytes32 node => bytes data)) private _versionZonehash; + + /** + * @dev Override {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceID) public view virtual override(BaseVersion, ERC165) returns (bool) { + return interfaceID == type(IDNSRecordResolver).interfaceId || interfaceID == type(IDNSZoneResolver).interfaceId + || super.supportsInterface(interfaceID); + } + + /** + * @dev Checks whether a given node has records. + * @param node the namehash of the node for which to check the records + * @param name the namehash of the node for which to check the records + */ + function hasDNSRecords(bytes32 node, bytes32 name) public view virtual returns (bool) { + return (_versionNameEntriesCount[_recordVersion[node]][node][name] != 0); + } + + /** + * @inheritdoc IDNSRecordResolver + */ + function dnsRecord(bytes32 node, bytes32 name, uint16 resource) public view virtual override returns (bytes memory) { + return _versionRecord[_recordVersion[node]][node][name][resource]; + } + + /** + * @inheritdoc IDNSZoneResolver + */ + function zonehash(bytes32 node) external view virtual override returns (bytes memory) { + return _versionZonehash[_recordVersion[node]][node]; + } + + /** + * @dev See {IDNSRecordResolver-setDNSRecords}. + */ + function _setDNSRecords(bytes32 node, bytes calldata data) internal { + uint16 resource = 0; + uint256 offset = 0; + bytes memory name; + bytes memory value; + bytes32 nameHash; + uint64 version = _recordVersion[node]; + // Iterate over the data to add the resource records + for (RRUtils.RRIterator memory iter = data.iterateRRs(0); !iter.done(); iter.next()) { + if (resource == 0) { + resource = iter.dnstype; + name = iter.name(); + nameHash = keccak256(abi.encodePacked(name)); + value = bytes(iter.rdata()); + } else { + bytes memory newName = iter.name(); + if (resource != iter.dnstype || !name.equals(newName)) { + _setDNSRRSet(node, name, resource, data, offset, iter.offset - offset, value.length == 0, version); + resource = iter.dnstype; + offset = iter.offset; + name = newName; + nameHash = keccak256(name); + value = bytes(iter.rdata()); + } + } + } + + if (name.length > 0) { + _setDNSRRSet(node, name, resource, data, offset, data.length - offset, value.length == 0, version); + } + } + + /** + * @dev See {IDNSZoneResolver-setZonehash}. + */ + function _setZonehash(bytes32 node, bytes calldata hash) internal { + uint64 currentRecordVersion = _recordVersion[node]; + bytes memory oldhash = _versionZonehash[currentRecordVersion][node]; + _versionZonehash[currentRecordVersion][node] = hash; + emit DNSZonehashChanged(node, oldhash, hash); + } + + /** + * @dev Helper method to set DNS config. + * + * May emit an event {DNSRecordDeleted}. + * May emit an event {DNSRecordChanged}. + * + */ + function _setDNSRRSet( + bytes32 node, + bytes memory name, + uint16 resource, + bytes memory data, + uint256 offset, + uint256 size, + bool deleteRecord, + uint64 version + ) private { + bytes32 nameHash = keccak256(name); + bytes memory rrData = data.substring(offset, size); + if (deleteRecord) { + if (_versionRecord[version][node][nameHash][resource].length != 0) { + _versionNameEntriesCount[version][node][nameHash]--; + } + delete (_versionRecord[version][node][nameHash][resource]); + emit DNSRecordDeleted(node, name, resource); + } else { + if (_versionRecord[version][node][nameHash][resource].length == 0) { + _versionNameEntriesCount[version][node][nameHash]++; + } + _versionRecord[version][node][nameHash][resource] = rrData; + emit DNSRecordChanged(node, name, resource, rrData); + } + } +} diff --git a/src/resolvers/InterfaceResolvable.sol b/src/resolvers/InterfaceResolvable.sol new file mode 100644 index 00000000..84671603 --- /dev/null +++ b/src/resolvers/InterfaceResolvable.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import { BaseVersion } from "./BaseVersion.sol"; +import { IInterfaceResolver } from "@rns-contracts/interfaces/resolvers/IInterfaceResolver.sol"; + +abstract contract InterfaceResolvable is IInterfaceResolver, ERC165, BaseVersion { + /// @dev Gap for upgradeability. + uint256[50] private ____gap; + + /// @dev Mapping from version => node => interfaceID => address + mapping(uint64 version => mapping(bytes32 node => mapping(bytes4 interfaceID => address addr))) internal + _versionInterface; + + /** + * @dev Override {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceID) public view virtual override(BaseVersion, ERC165) returns (bool) { + return interfaceID == type(IInterfaceResolver).interfaceId || super.supportsInterface(interfaceID); + } + + /** + * @inheritdoc IInterfaceResolver + */ + function interfaceImplementer(bytes32 node, bytes4 interfaceID) external view virtual override returns (address) { + address implementer = _versionInterface[_recordVersion[node]][node][interfaceID]; + if (implementer != address(0)) return implementer; + + address addrOfNode = addr(node); + if (addrOfNode == address(0)) return address(0); + + bool success; + bytes memory returnData; + + (success, returnData) = + addrOfNode.staticcall(abi.encodeCall(IERC165.supportsInterface, (type(IERC165).interfaceId))); + + // EIP 165 not supported by target + if (!_isValidReturnData(success, returnData)) return address(0); + + (success, returnData) = addrOfNode.staticcall(abi.encodeCall(IERC165.supportsInterface, (interfaceID))); + // Specified interface not supported by target + if (!_isValidReturnData(success, returnData)) return address(0); + + return addrOfNode; + } + + /** + * @dev See {IAddressResolver-addr}. + */ + function addr(bytes32 node) public view virtual returns (address payable); + + /** + * @dev Checks whether the return data is valid. + */ + function _isValidReturnData(bool success, bytes memory returnData) internal pure returns (bool) { + return success || returnData.length < 32 || returnData[31] == 0; + } + + /** + * @dev See {InterfaceResolver-setInterface}. + */ + function _setInterface(bytes32 node, bytes4 interfaceID, address implementer) internal virtual { + _versionInterface[_recordVersion[node]][node][interfaceID] = implementer; + emit InterfaceChanged(node, interfaceID, implementer); + } +} diff --git a/src/resolvers/NameResolvable.sol b/src/resolvers/NameResolvable.sol new file mode 100644 index 00000000..11b50824 --- /dev/null +++ b/src/resolvers/NameResolvable.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { BaseVersion } from "./BaseVersion.sol"; +import { INameResolver } from "@rns-contracts/interfaces/resolvers/INameResolver.sol"; + +abstract contract NameResolvable is INameResolver, BaseVersion { + /// @dev Gap for upgradeability. + uint256[50] private ____gap; + + /// @dev mapping from version => node => name + mapping(uint64 version => mapping(bytes32 node => string name)) internal _versionName; + + /** + * @dev Override {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(INameResolver).interfaceId || super.supportsInterface(interfaceID); + } + + /** + * @inheritdoc INameResolver + */ + function name(bytes32 node) public view virtual override returns (string memory) { + return _versionName[_recordVersion[node]][node]; + } + + /** + * @dev See {INameResolver-setName}. + */ + function _setName(bytes32 node, string memory newName) internal virtual { + _versionName[_recordVersion[node]][node] = newName; + emit NameChanged(node, newName); + } +} diff --git a/src/resolvers/PublicKeyResolvable.sol b/src/resolvers/PublicKeyResolvable.sol new file mode 100644 index 00000000..e2e26b64 --- /dev/null +++ b/src/resolvers/PublicKeyResolvable.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { BaseVersion } from "./BaseVersion.sol"; +import { IPublicKeyResolver } from "@rns-contracts/interfaces/resolvers/IPublicKeyResolver.sol"; + +abstract contract PublicKeyResolvable is BaseVersion, IPublicKeyResolver { + /// @dev Gap for upgradeability. + uint256[50] private ____gap; + + /// @dev Mapping from version => node => public key + mapping(uint64 version => mapping(bytes32 node => PublicKey publicKey)) internal _versionPublicKey; + + /** + * @dev Override {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(IPublicKeyResolver).interfaceId || super.supportsInterface(interfaceID); + } + + /** + * @dev See {IPublicKeyResolver-pubkey}. + */ + function pubkey(bytes32 node) external view virtual override returns (bytes32 x, bytes32 y) { + uint64 currentRecordVersion = _recordVersion[node]; + return (_versionPublicKey[currentRecordVersion][node].x, _versionPublicKey[currentRecordVersion][node].y); + } + + /** + * @dev See {IPublicKeyResolver-setPubkey}. + */ + function _setPubkey(bytes32 node, bytes32 x, bytes32 y) internal virtual { + _versionPublicKey[_recordVersion[node]][node] = PublicKey(x, y); + emit PubkeyChanged(node, x, y); + } +} diff --git a/src/resolvers/PublicResolver.sol b/src/resolvers/PublicResolver.sol new file mode 100644 index 00000000..6f653816 --- /dev/null +++ b/src/resolvers/PublicResolver.sol @@ -0,0 +1,190 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { IVersionResolver } from "@rns-contracts/interfaces/resolvers/IVersionResolver.sol"; +import { Multicallable } from "@rns-contracts/extensions/Multicallable.sol"; +import { USER_FIELDS_INDICATOR } from "../types/ModifyingIndicator.sol"; +import { ABIResolvable } from "./ABIResolvable.sol"; +import { AddressResolvable } from "./AddressResolvable.sol"; +import { ContentHashResolvable } from "./ContentHashResolvable.sol"; +import { DNSResolvable } from "./DNSResolvable.sol"; +import { InterfaceResolvable } from "./InterfaceResolvable.sol"; +import { NameResolvable } from "./NameResolvable.sol"; +import { PublicKeyResolvable } from "./PublicKeyResolvable.sol"; +import { TextResolvable } from "./TextResolvable.sol"; +import "@rns-contracts/interfaces/resolvers/IPublicResolver.sol"; + +/** + * @title Public Resolver + * @notice Customized version of PublicResolver: https://github.com/ensdomains/ens-contracts/blob/0c75ba23fae76165d51c9c80d76d22261e06179d/contracts/resolvers/PublicResolver.sol + * @dev A simple resolver anyone can use, only allows the owner of a node to set its address. + */ +contract PublicResolver is + IPublicResolver, + ABIResolvable, + AddressResolvable, + ContentHashResolvable, + DNSResolvable, + InterfaceResolvable, + NameResolvable, + PublicKeyResolvable, + TextResolvable, + Multicallable, + Initializable +{ + /// @dev Gap for upgradeability. + uint256[50] private ____gap; + + /// @dev The RNS Unified contract + INSUnified internal _rnsUnified; + + /// @dev The reverse registrar contract + IReverseRegistrar internal _reverseRegistrar; + + modifier onlyAuthorized(bytes32 node) { + _requireAuthorized(node, msg.sender); + _; + } + + constructor() payable { + _disableInitializers(); + } + + function initialize(INSUnified rnsUnified, IReverseRegistrar reverseRegistrar) external initializer { + _rnsUnified = rnsUnified; + _reverseRegistrar = reverseRegistrar; + } + + /** + * @dev Override {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceID) + public + view + override( + ABIResolvable, + AddressResolvable, + ContentHashResolvable, + DNSResolvable, + InterfaceResolvable, + NameResolvable, + PublicKeyResolvable, + TextResolvable, + Multicallable + ) + returns (bool) + { + return super.supportsInterface(interfaceID); + } + + /// @inheritdoc IPublicResolver + function getRNSUnified() external view returns (INSUnified) { + return _rnsUnified; + } + + /// @inheritdoc IPublicResolver + function getReverseRegistrar() external view returns (IReverseRegistrar) { + return _reverseRegistrar; + } + + /// @inheritdoc IPublicResolver + function multicallWithNodeCheck(bytes32 node, bytes[] calldata data) + external + override + returns (bytes[] memory results) + { + if (node != 0) { + for (uint256 i; i < data.length;) { + require(node == bytes32(data[i][4:36]), "PublicResolver: All records must have a matching namehash"); + unchecked { + ++i; + } + } + } + + return _tryMulticall(true, data); + } + + /// @inheritdoc IVersionResolver + function clearRecords(bytes32 node) external onlyAuthorized(node) { + _clearRecords(node); + } + + /// @inheritdoc IABIResolver + function setABI(bytes32 node, uint256 contentType, bytes calldata data) external onlyAuthorized(node) { + _setABI(node, contentType, data); + } + + /// @inheritdoc IAddressResolver + function setAddr(bytes32 node, address addr_) external onlyAuthorized(node) { + revert("PublicResolver: Cannot set address"); + _setAddr(node, addr_); + } + + /// @inheritdoc IContentHashResolver + function setContentHash(bytes32 node, bytes calldata hash) external onlyAuthorized(node) { + _setContentHash(node, hash); + } + + /// @inheritdoc IDNSRecordResolver + function setDNSRecords(bytes32 node, bytes calldata data) external onlyAuthorized(node) { + _setDNSRecords(node, data); + } + + /// @inheritdoc IDNSZoneResolver + function setZonehash(bytes32 node, bytes calldata hash) external onlyAuthorized(node) { + _setZonehash(node, hash); + } + + /// @inheritdoc IInterfaceResolver + function setInterface(bytes32 node, bytes4 interfaceID, address implementer) external onlyAuthorized(node) { + _setInterface(node, interfaceID, implementer); + } + + /// @inheritdoc INameResolver + function setName(bytes32 node, string calldata newName) external onlyAuthorized(node) { + _setName(node, newName); + } + + /// @inheritdoc IPublicKeyResolver + function setPubkey(bytes32 node, bytes32 x, bytes32 y) external onlyAuthorized(node) { + _setPubkey(node, x, y); + } + + /// @inheritdoc ITextResolver + function setText(bytes32 node, string calldata key, string calldata value) external onlyAuthorized(node) { + _setText(node, key, value); + } + + /// @inheritdoc IPublicResolver + function isAuthorized(bytes32 node, address account) public view returns (bool authorized) { + (authorized,) = _rnsUnified.canSetRecord(account, uint256(node), USER_FIELDS_INDICATOR); + } + + /// @dev Override {IAddressResolvable-addr}. + function addr(bytes32 node) + public + view + virtual + override(AddressResolvable, IAddressResolver, InterfaceResolvable) + returns (address payable) + { + return payable(_rnsUnified.ownerOf(uint256(node))); + } + + /// @dev Override {INameResolver-name}. + function name(bytes32 node) public view virtual override(INameResolver, NameResolvable) returns (string memory) { + address reversedAddress = _reverseRegistrar.getAddress(node); + string memory domainName = super.name(node); + uint256 tokenId = uint256(_rnsUnified.namehash(domainName)); + return _rnsUnified.ownerOf(tokenId) == reversedAddress ? domainName : ""; + } + + /** + * @dev Reverts if the msg sender is not authorized. + */ + function _requireAuthorized(bytes32 node, address account) internal view { + require(isAuthorized(node, account), "PublicResolver: unauthorized caller"); + } +} diff --git a/src/resolvers/TextResolvable.sol b/src/resolvers/TextResolvable.sol new file mode 100644 index 00000000..92b0290c --- /dev/null +++ b/src/resolvers/TextResolvable.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { BaseVersion } from "./BaseVersion.sol"; +import { ITextResolver } from "@rns-contracts/interfaces/resolvers/ITextResolver.sol"; + +abstract contract TextResolvable is BaseVersion, ITextResolver { + /// @dev Gap for upgradeability. + uint256[50] private ____gap; + /// @dev Mapping from version => node => key => text + mapping(uint64 version => mapping(bytes32 node => mapping(string key => string text))) internal _versionText; + + /** + * @dev Override {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { + return interfaceID == type(ITextResolver).interfaceId || super.supportsInterface(interfaceID); + } + + /** + * @inheritdoc ITextResolver + */ + function text(bytes32 node, string calldata key) external view virtual override returns (string memory) { + return _versionText[_recordVersion[node]][node][key]; + } + + /** + * @dev See {ITextResolver-setText}. + */ + function _setText(bytes32 node, string calldata key, string calldata value) internal virtual { + _versionText[_recordVersion[node]][node][key] = value; + emit TextChanged(node, key, key, value); + } +} From 80d4738acb7d44a4f6dd4798702761595fe6b299 Mon Sep 17 00:00:00 2001 From: Duc Tho Tran Date: Thu, 12 Oct 2023 17:22:29 +0700 Subject: [PATCH 3/4] chore: resolve confict --- .gitmodules | 3 +++ remappings.txt | 1 + src/libraries/LibRNSDomain.sol | 31 +++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+) 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/remappings.txt b/remappings.txt index 1873e138..c7a9b45f 100644 --- a/remappings.txt +++ b/remappings.txt @@ -5,3 +5,4 @@ forge-std/=lib/forge-std/src/ contract-template/=lib/contract-template/src/ @ensdomains/ens-contracts/=lib/ens-contracts/contracts/ @ensdomains/buffer/=lib/buffer/ +@pythnetwork/=lib/pyth-sdk-solidity/ 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; + } + } } From 0ddf63120baa981279d4c7e8d0b3f380b73d15f1 Mon Sep 17 00:00:00 2001 From: Tu Do Date: Fri, 13 Oct 2023 11:21:18 +0700 Subject: [PATCH 4/4] feat: return tax price in `RNSDomainPrice` (#34) * feat: return usdTax and ronTax * feat: use struct --- src/RNSDomainPrice.sol | 11 ++++++----- src/interfaces/INSDomainPrice.sol | 7 ++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/RNSDomainPrice.sol b/src/RNSDomainPrice.sol index 382d14c3..ba64d3e3 100644 --- a/src/RNSDomainPrice.sol +++ b/src/RNSDomainPrice.sol @@ -240,24 +240,25 @@ contract RNSDomainPrice is Initializable, AccessControlEnumerable, INSDomainPric function getRenewalFee(string memory label, uint256 duration) public view - returns (uint256 usdPrice, uint256 ronPrice) + returns (UnitPrice memory basePrice, UnitPrice memory tax) { uint256 nameLen = label.strlen(); bytes32 lbHash = label.hashLabel(); uint256 overriddenRenewalFee = _rnFeeOverriding[lbHash]; if (overriddenRenewalFee != 0) { - usdPrice = duration * ~overriddenRenewalFee; + basePrice.usd = duration * ~overriddenRenewalFee; } else { uint256 renewalFeeByLength = _rnFee[Math.min(nameLen, _rnfMaxLength)]; - usdPrice = duration * renewalFeeByLength; + basePrice.usd = duration * renewalFeeByLength; // tax is added of name is reserved for auction if (_auction.reserved(LibRNSDomain.toId(LibRNSDomain.RON_ID, label))) { - usdPrice += Math.mulDiv(_taxRatio, _getDomainPrice(lbHash), MAX_PERCENTAGE); + tax.usd = Math.mulDiv(_taxRatio, _getDomainPrice(lbHash), MAX_PERCENTAGE); } } - ronPrice = convertUSDToRON(usdPrice); + tax.ron = convertUSDToRON(tax.usd); + basePrice.ron = convertUSDToRON(basePrice.usd); } /** diff --git a/src/interfaces/INSDomainPrice.sol b/src/interfaces/INSDomainPrice.sol index b543c1d1..735a97fd 100644 --- a/src/interfaces/INSDomainPrice.sol +++ b/src/interfaces/INSDomainPrice.sol @@ -13,6 +13,11 @@ interface INSDomainPrice { 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. @@ -111,7 +116,7 @@ interface INSDomainPrice { function getRenewalFee(string calldata label, uint256 duration) external view - returns (uint256 usdPrice, uint256 ronPrice); + returns (UnitPrice memory basePrice, UnitPrice memory tax); /** * @dev Returns the renewal fee of a label. Reverts if not overridden.