diff --git a/script/20231205-deploy-upgrade-auction-and-deploy-rns-operation/20231205_UpgradeRNSAuctionAndDeployRNSOperation.s.sol b/script/20231205-deploy-upgrade-auction-and-deploy-rns-operation/20231205_UpgradeRNSAuctionAndDeployRNSOperation.s.sol index d6851b42..aee0b91f 100644 --- a/script/20231205-deploy-upgrade-auction-and-deploy-rns-operation/20231205_UpgradeRNSAuctionAndDeployRNSOperation.s.sol +++ b/script/20231205-deploy-upgrade-auction-and-deploy-rns-operation/20231205_UpgradeRNSAuctionAndDeployRNSOperation.s.sol @@ -5,9 +5,20 @@ import { console2 as console } from "forge-std/console2.sol"; import { ITransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import { ProxyAdmin } from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import { ContractKey } from "foundry-deployment-kit/configs/ContractConfig.sol"; -import { Network, Config__Mainnet20231205 } from "script/20231205-deploy-upgrade-auction-and-deploy-rns-operation/20231205_MainnetConfig.s.sol"; -import { INSAuction, RNSAuction, RNSUnified, Migration__20231123_UpgradeAuctionClaimeUnbiddedNames as UpgradeAuctionScript } from "script/20231123-upgrade-auction-claim-unbidded-names/20231123_UpgradeAuctionClaimUnbiddedNames.s.sol"; -import { RNSOperation, Migration__20231124_DeployRNSOperation as DeployRNSOperationScript } from "script/20231124-deploy-rns-operation/20231124_DeployRNSOperation.s.sol"; +import { + Network, + Config__Mainnet20231205 +} from "script/20231205-deploy-upgrade-auction-and-deploy-rns-operation/20231205_MainnetConfig.s.sol"; +import { + INSAuction, + RNSAuction, + RNSUnified, + Migration__20231123_UpgradeAuctionClaimeUnbiddedNames as UpgradeAuctionScript +} from "script/20231123-upgrade-auction-claim-unbidded-names/20231123_UpgradeAuctionClaimUnbiddedNames.s.sol"; +import { + RNSOperation, + Migration__20231124_DeployRNSOperation as DeployRNSOperationScript +} from "script/20231124-deploy-rns-operation/20231124_DeployRNSOperation.s.sol"; contract Migration__20231205_UpgradeRNSAuctionAndDeployRNSOperation is Config__Mainnet20231205 { function run() public trySetUp onMainnet { diff --git a/src/RNSAuction.sol b/src/RNSAuction.sol index 86f66cfa..2a5d41a2 100644 --- a/src/RNSAuction.sol +++ b/src/RNSAuction.sol @@ -309,7 +309,6 @@ contract RNSAuction is Initializable, AccessControlEnumerable, INSAuction { /** * @inheritdoc INSAuction */ - function setBidGapRatio(uint256 ratio) external onlyRole(DEFAULT_ADMIN_ROLE) { _setBidGapRatio(ratio); } diff --git a/src/RNSDomainPrice.sol b/src/RNSDomainPrice.sol index 0187f2a6..92f21057 100644 --- a/src/RNSDomainPrice.sol +++ b/src/RNSDomainPrice.sol @@ -21,6 +21,10 @@ contract RNSDomainPrice is Initializable, AccessControlEnumerable, INSDomainPric using LibPeriodScaler for PeriodScaler; using PythConverter for PythStructs.Price; + /// @dev The threshold tier value (in USD) for Tier 1: > $200 + uint256 private constant TIER_1_FROM_EXCLUDED_THRESHOLD = 200e18; + /// @dev The threshold tier value (in USD) for Tier 2 in range of ($50; $200] + uint256 private constant TIER_2_FROM_EXCLUDED_THRESHOLD = 50e18; /// @inheritdoc INSDomainPrice uint8 public constant USD_DECIMALS = 18; /// @inheritdoc INSDomainPrice @@ -54,6 +58,8 @@ contract RNSDomainPrice is Initializable, AccessControlEnumerable, INSDomainPric mapping(bytes32 lbHash => TimestampWrapper usdPrice) internal _dp; /// @dev Mapping from name => inverse bitwise of renewal fee overriding. mapping(bytes32 lbHash => uint256 usdPrice) internal _rnFeeOverriding; + /// @dev Mapping from label hash to overriden tier + mapping(bytes32 lbHash => uint8 tier) internal _tierOverriding; constructor() payable { _disableInitializers(); @@ -167,6 +173,15 @@ contract RNSDomainPrice is Initializable, AccessControlEnumerable, INSDomainPric return ~usdFee; } + /** + * @inheritdoc INSDomainPrice + */ + function getOverriddenTier(string calldata label) external view returns (Tier tier) { + uint8 tierValue = _tierOverriding[label.hashLabel()]; + if (tierValue == 0) revert TierIsNotOverriden(); + return Tier(~tierValue); + } + /** * @inheritdoc INSDomainPrice */ @@ -190,6 +205,26 @@ contract RNSDomainPrice is Initializable, AccessControlEnumerable, INSDomainPric } } + /** + * @inheritdoc INSDomainPrice + */ + function bulkOverrideTiers(bytes32[] calldata lbHashes, Tier[] calldata tiers) external onlyRole(OVERRIDER_ROLE) { + uint256 length = lbHashes.length; + if (length == 0 || length != tiers.length) revert InvalidArrayLength(); + uint8 inverseBitwise; + address operator = _msgSender(); + + for (uint256 i; i < length;) { + inverseBitwise = ~uint8(tiers[i]); + _tierOverriding[lbHashes[i]] = inverseBitwise; + emit TierOverridingUpdated(operator, lbHashes[i], tiers[i]); + + unchecked { + ++i; + } + } + } + /** * @inheritdoc INSDomainPrice */ @@ -240,6 +275,27 @@ contract RNSDomainPrice is Initializable, AccessControlEnumerable, INSDomainPric ronPrice = convertUSDToRON(usdPrice); } + /** + * @inheritdoc INSDomainPrice + */ + function getTier(string memory label) public view returns (Tier tier) { + bytes32 lbHash = label.hashLabel(); + uint8 overriddenTier = _tierOverriding[lbHash]; + + if (overriddenTier != 0) return Tier(~overriddenTier); + + (UnitPrice memory yearlyRenewalFeeByLength,,) = _tryGetRenewalFee({ label: label, duration: 365 days }); + uint256 tierValue = yearlyRenewalFeeByLength.usd + _getDomainPrice(lbHash) / 2; + + if (tierValue > TIER_1_FROM_EXCLUDED_THRESHOLD) { + return Tier.Tier1; + } else if (tierValue > TIER_2_FROM_EXCLUDED_THRESHOLD) { + return Tier.Tier2; + } else { + return Tier.Tier3; + } + } + /** * @inheritdoc INSDomainPrice */ @@ -248,32 +304,14 @@ contract RNSDomainPrice is Initializable, AccessControlEnumerable, INSDomainPric view returns (UnitPrice memory basePrice, UnitPrice memory tax) { - uint256 nameLen = label.strlen(); - bytes32 lbHash = label.hashLabel(); - uint256 overriddenRenewalFee = _rnFeeOverriding[lbHash]; - - if (overriddenRenewalFee != 0) { - basePrice.usd = duration * ~overriddenRenewalFee; - } else { - uint256 renewalFeeByLength = _rnFee[Math.min(nameLen, _rnfMaxLength)]; - basePrice.usd = duration * renewalFeeByLength; - uint256 id = LibRNSDomain.toId(LibRNSDomain.RON_ID, label); - INSAuction auction = _auction; - if (auction.reserved(id)) { - INSUnified rns = auction.getRNSUnified(); - uint256 expiry = LibSafeRange.addWithUpperbound(rns.getRecord(id).mut.expiry, duration, type(uint64).max); - (INSAuction.DomainAuction memory domainAuction,) = auction.getAuction(id); - uint256 claimedAt = domainAuction.bid.claimedAt; - if (claimedAt != 0 && expiry - claimedAt > auction.MAX_AUCTION_DOMAIN_EXPIRY()) { - revert ExceedAuctionDomainExpiry(); - } - // Tax is added to the name reserved for the auction - tax.usd = Math.mulDiv(_taxRatio, _getDomainPrice(lbHash), MAX_PERCENTAGE); + bytes4 revertReason; + (basePrice, tax, revertReason) = _tryGetRenewalFee(label, duration); + if (revertReason != bytes4(0x0)) { + assembly ("memory-safe") { + mstore(0x0, revertReason) + revert(0x0, 0x04) } } - - tax.ron = convertUSDToRON(tax.usd); - basePrice.ron = convertUSDToRON(basePrice.usd); } /** @@ -398,6 +436,48 @@ contract RNSDomainPrice is Initializable, AccessControlEnumerable, INSDomainPric emit PythOracleConfigUpdated(_msgSender(), pyth, maxAcceptableAge, pythIdForRONUSD); } + /** + * @dev Tries to get the renewal fee for a given domain label and duration. + * It returns the base price, tax, and a revert reason if applicable. + * @param label The domain label. + * @param duration The duration for which the domain is being renewed. + * @return basePrice The base price in USD for ˝renewing the domain. + * @return tax The tax amount in USD for renewing the domain. + * @return revertReason The revert reason if the renewal fee exceeds the auction domain expiry. + */ + function _tryGetRenewalFee(string memory label, uint256 duration) + internal + view + returns (UnitPrice memory basePrice, UnitPrice memory tax, bytes4 revertReason) + { + uint256 nameLen = label.strlen(); + bytes32 lbHash = label.hashLabel(); + uint256 overriddenRenewalFee = _rnFeeOverriding[lbHash]; + + if (overriddenRenewalFee != 0) { + basePrice.usd = duration * ~overriddenRenewalFee; + } else { + uint256 renewalFeeByLength = _rnFee[Math.min(nameLen, _rnfMaxLength)]; + basePrice.usd = duration * renewalFeeByLength; + uint256 id = LibRNSDomain.toId(LibRNSDomain.RON_ID, label); + INSAuction auction = _auction; + if (auction.reserved(id)) { + INSUnified rns = auction.getRNSUnified(); + uint256 expiry = LibSafeRange.addWithUpperbound(rns.getRecord(id).mut.expiry, duration, type(uint64).max); + (INSAuction.DomainAuction memory domainAuction,) = auction.getAuction(id); + uint256 claimedAt = domainAuction.bid.claimedAt; + if (claimedAt != 0 && expiry - claimedAt > auction.MAX_AUCTION_DOMAIN_EXPIRY()) { + return (basePrice, tax, ExceedAuctionDomainExpiry.selector); + } + // Tax is added to the name reserved for the auction + tax.usd = Math.mulDiv(_taxRatio, _getDomainPrice(lbHash), MAX_PERCENTAGE); + } + } + + tax.ron = convertUSDToRON(tax.usd); + basePrice.ron = convertUSDToRON(basePrice.usd); + } + /** * @dev Returns the current domain price applied the business rule: deduced x% each y seconds. */ diff --git a/src/interfaces/INSDomainPrice.sol b/src/interfaces/INSDomainPrice.sol index 376284fe..59586748 100644 --- a/src/interfaces/INSDomainPrice.sol +++ b/src/interfaces/INSDomainPrice.sol @@ -7,8 +7,17 @@ import { IPyth } from "@pythnetwork/IPyth.sol"; interface INSDomainPrice { error InvalidArrayLength(); error RenewalFeeIsNotOverriden(); + error TierIsNotOverriden(); error ExceedAuctionDomainExpiry(); + /// @dev The tier of a domain. + enum Tier { + Unknown, + Tier1, + Tier2, + Tier3 + } + struct RenewalFee { uint256 labelLength; uint256 fee; @@ -27,6 +36,8 @@ interface INSDomainPrice { 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 tier of a domain is overridden. + event TierOverridingUpdated(address indexed operator, bytes32 indexed labelHash, Tier indexed tier); /// @dev Emitted when the domain price is updated. event DomainPriceUpdated( @@ -119,6 +130,13 @@ interface INSDomainPrice { view returns (UnitPrice memory basePrice, UnitPrice memory tax); + /** + * @dev Returns the tier of a label. + * @param label The domain label to register (Eg, 'foo' for 'foo.ron'). + * @return tier The tier of the label. + */ + function getTier(string calldata label) external view returns (Tier tier); + /** * @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 @@ -126,6 +144,13 @@ interface INSDomainPrice { */ function getOverriddenRenewalFee(string memory label) external view returns (uint256 usdFee); + /** + * @dev Returns the tier of a label. Reverts if not overridden. + * @notice This method is to help developers check the domain tier overriding. Consider using method {getTier} instead + * for full handling of tiers. + */ + function getOverriddenTier(string memory label) external view returns (Tier tier); + /** * @dev Bulk override renewal fees. * @@ -140,6 +165,20 @@ interface INSDomainPrice { */ function bulkOverrideRenewalFees(bytes32[] calldata lbHashes, uint256[] calldata usdPrices) external; + /** + * @dev Bulk override tiers. + * + * Requirements: + * - The method caller is operator. + * - The input array lengths must be larger than 0 and the same. + * + * Emits events {TierOverridingUpdated}. + * + * @param lbHashes Array of label hashes. (Eg, ['foo'].map(keccak256) for 'foo.ron') + * @param tiers Array of tiers. Leave 2^256 - 1 to remove overriding. + */ + function bulkOverrideTiers(bytes32[] calldata lbHashes, Tier[] calldata tiers) external; + /** * @dev Bulk try to set domain prices. Returns a boolean array indicating whether domain prices at the corresponding * indexes if set or not. diff --git a/src/utils/RNSOperation.sol b/src/utils/RNSOperation.sol index cdfcbb0f..8365bb49 100644 --- a/src/utils/RNSOperation.sol +++ b/src/utils/RNSOperation.sol @@ -55,7 +55,7 @@ contract RNSOperation is Ownable { bytes32[] memory lbHashes = new bytes32[](labels.length); for (uint256 i; i < lbHashes.length; ++i) { - lbHashes[i] = keccak256(bytes(labels[i])); + lbHashes[i] = LibRNSDomain.hashLabel(labels[i]); } uint256[] memory usdPrices = new uint256[](yearlyUSDPrices.length); for (uint256 i; i < usdPrices.length; ++i) { @@ -65,6 +65,23 @@ contract RNSOperation is Ownable { domainPrice.bulkOverrideRenewalFees(lbHashes, usdPrices); } + /** + * @dev Allows the owner to bulk override the tiers for specified RNS domains. + * @param labels The array of labels for the RNS domains. + * @param tiers The array of tiers for the corresponding RNS domains. + * @dev The `tiers` array should represent the tiers for each domain. + */ + function bulkOverrideTiers(string[] calldata labels, INSDomainPrice.Tier[] calldata tiers) external onlyOwner { + require(labels.length == tiers.length, "RNSOperation: length mismatch"); + + bytes32[] memory lbHashes = new bytes32[](labels.length); + for (uint256 i; i < lbHashes.length; ++i) { + lbHashes[i] = LibRNSDomain.hashLabel(labels[i]); + } + + domainPrice.bulkOverrideTiers(lbHashes, tiers); + } + /** * @dev Allows the owner to reclaim unbidded RNS domain names and transfer them to specified addresses. * @param tos The array of addresses to which the unbidded domains will be transferred. diff --git a/test/RNSUnified/RNSUnified.namehash.t.sol b/test/RNSUnified/RNSUnified.namehash.t.sol index 6f2e8f70..2d3b537f 100644 --- a/test/RNSUnified/RNSUnified.namehash.t.sol +++ b/test/RNSUnified/RNSUnified.namehash.t.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.19; import "./RNSUnified.t.sol"; -import { LibString } from "solady/utils/LibString.sol"; +import { LibString as SoladyLibString } from "solady/utils/LibString.sol"; contract RNSUnified_NameHash_Test is RNSUnifiedTest { - using LibString for *; + using SoladyLibString for *; function testGas_namehash(string calldata domainName) external view { _rns.namehash(domainName); diff --git a/test/RNSUnified/RNSUnified.t.sol b/test/RNSUnified/RNSUnified.t.sol index e541a2a3..7522c901 100644 --- a/test/RNSUnified/RNSUnified.t.sol +++ b/test/RNSUnified/RNSUnified.t.sol @@ -96,7 +96,13 @@ abstract contract RNSUnifiedTest is Test { address logic = address(new RNSUnified()); _rns = RNSUnified( address( - new TransparentUpgradeableProxy(logic, _proxyAdmin, abi.encodeCall(RNSUnified.initialize, (_admin, _pauser, _controller, _protectedSettler, GRACE_PERIOD, BASE_URI))) + new TransparentUpgradeableProxy( + logic, + _proxyAdmin, + abi.encodeCall( + RNSUnified.initialize, (_admin, _pauser, _controller, _protectedSettler, GRACE_PERIOD, BASE_URI) + ) + ) ) );