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/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/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/lib/solady b/lib/solady new file mode 160000 index 00000000..2ba1cc1e --- /dev/null +++ b/lib/solady @@ -0,0 +1 @@ +Subproject commit 2ba1cc1eaa3bffd5c093d94f76ef1b87b167ff3c 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/NameChecker.sol b/src/NameChecker.sol new file mode 100644 index 00000000..267da96d --- /dev/null +++ b/src/NameChecker.sol @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { AccessControlEnumerable } from "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; +import { Math, LibSubString } from "./libraries/LibSubString.sol"; +import { INameChecker } from "./interfaces/INameChecker.sol"; + +contract NameChecker is Initializable, AccessControlEnumerable, INameChecker { + using LibSubString for *; + using BitMaps for BitMaps.BitMap; + + /// @dev Gap for upgradeability. + uint256[50] private ____gap; + + LibSubString.WordRange internal _wordRange; + BitMaps.BitMap internal _forbiddenWordMap; + + constructor() payable { + _disableInitializers(); + } + + function initialize(address admin, uint8 min, uint8 max) external initializer { + _setupRole(DEFAULT_ADMIN_ROLE, admin); + _setWordRange(min, max); + } + + /** + * @inheritdoc INameChecker + */ + function getWordRange() external view returns (uint8 min, uint8 max) { + LibSubString.WordRange memory wordRange = _wordRange; + return (wordRange.min, wordRange.max); + } + + /** + * @inheritdoc INameChecker + */ + function setWordRange(uint8 min, uint8 max) external onlyRole(DEFAULT_ADMIN_ROLE) { + _setWordRange(min, max); + } + + /** + * @inheritdoc INameChecker + */ + function forbidden(string calldata name) public view returns (bool) { + return containsInvalidCharacter(name) || containsBlacklistedWord(name); + } + + /** + * @inheritdoc INameChecker + */ + function containsBlacklistedWord(string calldata name) public view returns (bool) { + string[] memory sstrs = getAllSubStrings(name); + uint256 length = sstrs.length; + + for (uint256 i; i < length;) { + if (_forbiddenWordMap.get(pack(sstrs[i]))) return true; + + unchecked { + ++i; + } + } + + return false; + } + + /** + * @inheritdoc INameChecker + */ + function containsInvalidCharacter(string calldata name) public pure returns (bool) { + unchecked { + bytes1 char; + bytes memory bName = bytes(name); + uint256 length = bName.length; + + uint256 tail = length - 1; + // Check if the name is empty or starts or ends with a hyphen (-) + if (length == 0 || bName[0] == 0x2d || bName[tail] == 0x2d) return true; + + // [0x30, 0x39] => [0-9] + // [0x61, 0x7a] => [a-z] + for (uint256 i; i < length; ++i) { + char = bName[i]; + if (char == 0x2d) { + // Check consecutive hyphens + if (i != tail && bName[i + 1] == 0x2d) return true; + } + // Check for invalid character (not (-) || [0-9] || [a-z]) + else if (!((char >= 0x30 && char <= 0x39) || (char >= 0x61 && char <= 0x7a))) { + return true; + } + } + + return false; + } + } + + /** + * @inheritdoc INameChecker + */ + function pack(string memory str) public pure returns (uint256 packed) { + assembly ("memory-safe") { + // We don't need to zero right pad the string, + // since this is our own custom non-standard packing scheme. + packed := + mul( + // Load the length and the bytes. + mload(add(str, 0x1f)), + // `length != 0 && length < 32`. Abuses underflow. + // Assumes that the length is valid and within the block gas limit. + lt(sub(mload(str), 1), 0x1f) + ) + } + } + + /** + * @inheritdoc INameChecker + */ + function packBulk(string[] memory strs) public pure returns (uint256[] memory packeds) { + uint256 length = strs.length; + packeds = new uint256[](length); + + for (uint256 i; i < length;) { + packeds[i] = pack(strs[i]); + + unchecked { + ++i; + } + } + } + + /** + * @inheritdoc INameChecker + */ + function setForbiddenWords(string[] calldata words, bool shouldForbid) external onlyRole(DEFAULT_ADMIN_ROLE) { + uint256[] memory packedWords = packBulk(words); + _setForbiddenWords(packedWords, shouldForbid); + } + + /** + * @inheritdoc INameChecker + */ + function setForbiddenWords(uint256[] calldata packedWords, bool shouldForbid) external onlyRole(DEFAULT_ADMIN_ROLE) { + _setForbiddenWords(packedWords, shouldForbid); + } + + /** + * @inheritdoc INameChecker + */ + function totalSubString(uint256 strlen) public view returns (uint256 total, uint256 min, uint256 max) { + (total, min, max) = strlen.totalSubString(_wordRange); + } + + /** + * @inheritdoc INameChecker + */ + function getAllSubStrings(string calldata str) public view returns (string[] memory subStrings) { + subStrings = str.getAllSubStrings(_wordRange); + } + + /** + * @dev Set the forbidden status of packed words. + * @param packedWords An array of packed word representations. + * @param shouldForbid A boolean flag indicating whether to forbid or unforbid the words. + * @notice It ensures that packed words are not zero, indicating their validity. + * @notice Emits a `ForbiddenWordsUpdated` event upon successful execution. + */ + function _setForbiddenWords(uint256[] memory packedWords, bool shouldForbid) internal { + uint256 length = packedWords.length; + uint256 strlen; + uint256 max; + uint256 min = type(uint256).max; + + for (uint256 i; i < length;) { + require(packedWords[i] != 0, "NameChecker: invalid packed word"); + strlen = packedWords[i] >> 0xf8; + min = Math.min(min, strlen); + max = Math.max(max, strlen); + _forbiddenWordMap.setTo(packedWords[i], shouldForbid); + + unchecked { + ++i; + } + } + + if (shouldForbid) { + LibSubString.WordRange memory wordRange = _wordRange; + min = Math.min(min, wordRange.min); + max = Math.max(max, wordRange.max); + if (!(min == wordRange.min && max == wordRange.max)) _setWordRange(uint8(min), uint8(max)); + } + + emit ForbiddenWordsUpdated(_msgSender(), length, shouldForbid); + } + + /** + * @dev Set the allowed word length range. + * @param min The minimum word length allowed. + * @param max The maximum word length allowed. + * @notice The minimum word length must be greater than 0, and it must not exceed the maximum word length. + */ + function _setWordRange(uint8 min, uint8 max) internal { + require(min != 0 && min <= max, "NameChecker: min word length > max word length"); + _wordRange = LibSubString.WordRange(min, max); + emit WordRangeUpdated(_msgSender(), min, max); + } +} diff --git a/src/RNSUnified.sol b/src/RNSUnified.sol index 134c1cba..00592ff2 100644 --- a/src/RNSUnified.sol +++ b/src/RNSUnified.sol @@ -128,10 +128,11 @@ contract RNSUnified is Initializable, RNSToken { } /// @inheritdoc INSUnified - function renew(uint256 id, uint64 duration) external whenNotPaused onlyRole(CONTROLLER_ROLE) { + function renew(uint256 id, uint64 duration) external whenNotPaused onlyRole(CONTROLLER_ROLE) returns (uint64 expiry) { Record memory record; record.mut.expiry = uint64(LibSafeRange.addWithUpperbound(_recordOf[id].mut.expiry, duration, MAX_EXPIRY)); _setExpiry(id, record.mut.expiry); + expiry = record.mut.expiry; emit RecordUpdated(id, ModifyingField.Expiry.indicator(), record); } diff --git a/src/RONRegistrarController.sol b/src/RONRegistrarController.sol new file mode 100644 index 00000000..3488fd9e --- /dev/null +++ b/src/RONRegistrarController.sol @@ -0,0 +1,445 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { Pausable } from "@openzeppelin/contracts/security/Pausable.sol"; +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import { AccessControlEnumerable } from "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; +import { IPublicResolver } from "./interfaces/resolvers/IPublicResolver.sol"; +import { + INSUnified, + INameChecker, + INSDomainPrice, + INSReverseRegistrar, + IRONRegistrarController +} from "./interfaces/IRONRegistrarController.sol"; +import { LibRNSDomain } from "./libraries/LibRNSDomain.sol"; +import { RONTransferHelper } from "./libraries/transfers/RONTransferHelper.sol"; + +/** + * @title RONRegistrarController + * @notice Customized version of ETHRegistrarController: https://github.com/ensdomains/ens-contracts/blob/45455f1229556ed4f416ef7225d4caea2c1bc0b5/contracts/ethregistrar/ETHRegistrarController.sol + * @dev A registrar controller for registering and renewing names at fixed cost. + */ +contract RONRegistrarController is + Pausable, + Initializable, + ReentrancyGuard, + AccessControlEnumerable, + IRONRegistrarController +{ + using LibRNSDomain for string; + + /// @dev The minimum domain name's length + uint8 public constant MIN_DOMAIN_LENGTH = 3; + /// @inheritdoc IRONRegistrarController + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + /// @inheritdoc IRONRegistrarController + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + + /// @dev Gap for upgradeability. + uint256[50] private ____gap; + + /// @dev Minimum duration between commitment and registration in second(s). + uint256 internal _minCommitmentAge; + /// @dev Maximum duration between commitment and registration in second(s). + uint256 internal _maxCommitmentAge; + /// @dev Min registration duration + uint256 internal _minRegistrationDuration; + + /// @dev The treasury address. + address payable internal _treasury; + /// @dev The rns unified contract. + INSUnified internal _rnsUnified; + /// @dev The namechecker contract. + INameChecker internal _nameChecker; + /// @dev The price oracle. + INSDomainPrice internal _priceOracle; + /// @dev The reverse registrar contract. + INSReverseRegistrar internal _reverseRegistrar; + + /// @dev Mapping from commitment hash => timestamp that commitment made. + mapping(bytes32 commitment => uint256 timestamp) internal _committedAt; + /// @dev Mapping id => owner => flag indicating whether the owner is whitelisted to buy protected name + mapping(uint256 id => mapping(address owner => bool)) internal _protectedNamesWhitelisted; + + constructor() payable { + _disableInitializers(); + } + + function initialize( + address admin, + address pauser, + address payable treasury, + uint256 maxCommitmentAge, + uint256 minCommitmentAge, + uint256 minRegistrationDuration, + INSUnified rnsUnified, + INameChecker nameChecker, + INSDomainPrice priceOracle, + INSReverseRegistrar reverseRegistrar + ) external initializer { + _setupRole(PAUSER_ROLE, pauser); + _setupRole(DEFAULT_ADMIN_ROLE, admin); + + _setPriceOracle(priceOracle); + _setMinRegistrationDuration(minRegistrationDuration); + _setCommitmentAge(minCommitmentAge, maxCommitmentAge); + + _treasury = treasury; + _rnsUnified = rnsUnified; + _nameChecker = nameChecker; + _reverseRegistrar = reverseRegistrar; + } + + /** + * @inheritdoc IRONRegistrarController + */ + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); + } + + /** + * @inheritdoc IRONRegistrarController + */ + function unpause() external onlyRole(PAUSER_ROLE) { + _unpause(); + } + + /** + * @inheritdoc IRONRegistrarController + */ + function getMinRegistrationDuration() public view returns (uint256) { + return _minRegistrationDuration; + } + + /** + * @inheritdoc IRONRegistrarController + */ + function rentPrice(string memory name, uint64 duration) public view returns (uint256 usdPrice, uint256 ronPrice) { + (usdPrice, ronPrice) = _priceOracle.getRenewalFee(name, duration); + } + + /** + * @inheritdoc IRONRegistrarController + */ + function valid(string memory name) public view returns (bool) { + return name.strlen() >= MIN_DOMAIN_LENGTH && !_nameChecker.forbidden(name); + } + + /** + * @inheritdoc IRONRegistrarController + */ + function available(string memory name) public view returns (bool) { + return valid(name) && _rnsUnified.available(computeId(name)); + } + + /** + * @inheritdoc IRONRegistrarController + */ + function computeCommitment( + string memory name, + address owner, + uint64 duration, + bytes32 secret, + address resolver, + bytes[] calldata data, + bool reverseRecord + ) public pure returns (bytes32) { + if (data.length != 0 && resolver == address(0)) revert ResolverRequiredWhenDataSupplied(); + return keccak256(abi.encode(computeId(name), owner, duration, secret, resolver, data, reverseRecord)); + } + + /** + * @inheritdoc IRONRegistrarController + */ + function computeId(string memory name) public pure returns (uint256 id) { + return LibRNSDomain.toId(LibRNSDomain.RON_ID, name); + } + + /** + * @inheritdoc IRONRegistrarController + */ + function commit(bytes32 commitment) external whenNotPaused { + if (_committedAt[commitment] + _maxCommitmentAge >= block.timestamp) revert UnexpiredCommitmentExists(commitment); + _committedAt[commitment] = block.timestamp; + } + + /** + * @inheritdoc IRONRegistrarController + */ + function setMinRegistrationDuration(uint256 duration) external onlyRole(DEFAULT_ADMIN_ROLE) { + _setMinRegistrationDuration(duration); + } + + /** + * @inheritdoc IRONRegistrarController + */ + function register( + string memory name, + address owner, + uint64 duration, + bytes32 secret, + address resolver, + bytes[] calldata data, + bool reverseRecord + ) external payable whenNotPaused nonReentrant { + uint256 id = computeId(name); + if (_rnsUnified.getRecord(id).mut.protected) revert ErrRequestedForProtectedName(name); + + bytes32 commitHash = computeCommitment({ + name: name, + owner: owner, + duration: duration, + secret: secret, + resolver: resolver, + data: data, + reverseRecord: reverseRecord + }); + _validateCommitment(name, duration, commitHash); + + (uint256 usdPrice, uint256 ronPrice) = _handlePrice(name, duration); + _register(name, owner, duration, resolver, data, reverseRecord, usdPrice, ronPrice); + } + + /** + * @inheritdoc IRONRegistrarController + */ + function renew(string calldata name, uint64 duration) external payable whenNotPaused nonReentrant { + (, uint256 ronPrice) = rentPrice(name, duration); + if (msg.value < ronPrice) revert InsufficientValue(); + uint256 remainAmount = msg.value - ronPrice; + + uint256 id = computeId(name); + uint64 expiryTime = _rnsUnified.renew(id, duration); + emit NameRenewed(name, id, ronPrice, expiryTime); + + if (remainAmount != 0) RONTransferHelper.safeTransfer(payable(_msgSender()), remainAmount); + _transferRONToTreasury(); + } + + /** + * @inheritdoc IRONRegistrarController + */ + function registerProtectedName( + string memory name, + address owner, + uint64 duration, + address resolver, + bytes[] calldata data, + bool reverseRecord + ) external payable whenNotPaused nonReentrant { + uint256 id = computeId(name); + bool protected = _rnsUnified.getRecord(id).mut.protected; + bool whitelisted = _protectedNamesWhitelisted[id][owner]; + if (!protected || !whitelisted) revert ErrInvalidRegisterProtectedName(name, owner, protected, whitelisted); + + (uint256 usdPrice, uint256 ronPrice) = _handlePrice(name, duration); + _register(name, owner, duration, resolver, data, reverseRecord, usdPrice, ronPrice); + } + + /** + * @inheritdoc IRONRegistrarController + */ + function bulkWhitelistProtectedNames(uint256[] calldata ids, address[] calldata owners, bool status) + external + onlyRole(OPERATOR_ROLE) + { + uint256 length = ids.length; + if (length == 0 || length != owners.length) revert InvalidArrayLength(); + + for (uint256 i; i < length;) { + _protectedNamesWhitelisted[ids[i]][owners[i]] = status; + + unchecked { + ++i; + } + } + + emit ProtectedNamesWhitelisted(_msgSender(), ids, owners, status); + } + + /** + * @inheritdoc IRONRegistrarController + */ + function getWhitelistProtectedNameStatus(uint256 id, address owner) external view returns (bool status) { + return _protectedNamesWhitelisted[id][owner]; + } + + /** + * @inheritdoc IRONRegistrarController + */ + function setTreasury(address payable addr) external onlyRole(DEFAULT_ADMIN_ROLE) { + _treasury = addr; + } + + /** + * @inheritdoc IRONRegistrarController + */ + function setCommitmentAge(uint256 minCommitmentAge, uint256 maxCommitmentAge) external onlyRole(DEFAULT_ADMIN_ROLE) { + _setCommitmentAge(minCommitmentAge, maxCommitmentAge); + } + + /** + * @dev Internal function to update the commitment age range. + * Requirements: + * - The `maxCommitmentAge` must be less than or equal to the current block timestamp. + * - The `maxCommitmentAge` must be greater than the `minCommitmentAge`. + * Emits a {CommitmentAgeUpdated} event indicating the successful update of the age range. + * @param minCommitmentAge The minimum commitment age in seconds. + * @param maxCommitmentAge The maximum commitment age in seconds. + */ + function _setCommitmentAge(uint256 minCommitmentAge, uint256 maxCommitmentAge) internal { + if (maxCommitmentAge > block.timestamp) revert MaxCommitmentAgeTooHigh(); + if (maxCommitmentAge <= minCommitmentAge) revert MaxCommitmentAgeTooLow(); + + _minCommitmentAge = minCommitmentAge; + _maxCommitmentAge = maxCommitmentAge; + + emit CommitmentAgeUpdated(_msgSender(), minCommitmentAge, maxCommitmentAge); + } + + /** + * @inheritdoc IRONRegistrarController + */ + function setPriceOracle(INSDomainPrice priceOracle) external onlyRole(DEFAULT_ADMIN_ROLE) { + _setPriceOracle(priceOracle); + } + + /** + * @inheritdoc IRONRegistrarController + */ + function getTreasury() external view returns (address) { + return _treasury; + } + + /** + * @inheritdoc IRONRegistrarController + */ + function getCommitmentAgeRange() external view returns (uint256 minCommitmentAge, uint256 maxCommitmentAge) { + return (_minCommitmentAge, _maxCommitmentAge); + } + + /** + * @inheritdoc IRONRegistrarController + */ + function getRNSUnified() external view returns (INSUnified) { + return _rnsUnified; + } + + /** + * @inheritdoc IRONRegistrarController + */ + function getPriceOracle() external view returns (INSDomainPrice) { + return _priceOracle; + } + + /** + * @inheritdoc IRONRegistrarController + */ + function getNameChecker() external view returns (INameChecker) { + return _nameChecker; + } + + /** + * @inheritdoc IRONRegistrarController + */ + function getReverseRegistrar() external view returns (INSReverseRegistrar) { + return _reverseRegistrar; + } + + /** + * @dev Validates commitment. + * + * Requirements: + * - The duration must larger than or equal to minimum registration duration. + * - The name must be available. + * - The passed duration must in a valid range. + */ + function _validateCommitment(string memory name, uint64 duration, bytes32 commitment) internal { + if (duration < _minRegistrationDuration) revert DurationTooShort(duration); + if (!available(name)) revert NameNotAvailable(name); + + uint256 passedDuration = block.timestamp - _committedAt[commitment]; + if (passedDuration < _minCommitmentAge) revert CommitmentTooNew(commitment); + if (_maxCommitmentAge < passedDuration) revert CommitmentTooOld(commitment); + + delete _committedAt[commitment]; + } + + /** + * @dev Sets minimum registration duration. + * Emits a {MinRegistrationDurationUpdated} event indicating the successful update of the registration duration. + */ + function _setMinRegistrationDuration(uint256 duration) internal { + _minRegistrationDuration = duration; + emit MinRegistrationDurationUpdated(_msgSender(), duration); + } + + /** + * @dev Sets data into resolver address contract. + */ + function _setRecords(address resolverAddress, uint256 id, bytes[] calldata data) internal { + IPublicResolver(resolverAddress).multicallWithNodeCheck(bytes32(id), data); + } + + /** + * @dev Sets data into reverse registrar. + */ + function _setReverseRecord(string memory name, address owner) internal { + _reverseRegistrar.setNameForAddr(owner, string.concat(name, ".ron")); + } + + /** + * @dev Helper method to take fee into treasury address. + */ + function _transferRONToTreasury() internal { + RONTransferHelper.safeTransfer(_treasury, address(this).balance); + } + + /** + * @dev Helper method to take renewal fee of a name. + */ + function _handlePrice(string memory name, uint64 duration) internal returns (uint256 usdPrice, uint256 ronPrice) { + (usdPrice, ronPrice) = rentPrice(name, duration); + if (msg.value < ronPrice) revert InsufficientValue(); + + unchecked { + uint256 remainAmount = msg.value - ronPrice; + if (remainAmount != 0) RONTransferHelper.safeTransfer(payable(_msgSender()), remainAmount); + } + + _transferRONToTreasury(); + } + + /** + * @dev Helper method to register a name for owner. + * + * Emits an event {NameRegistered}. + */ + function _register( + string memory name, + address owner, + uint64 duration, + address resolver, + bytes[] calldata data, + bool reverseRecord, + uint256 usdPrice, + uint256 ronPrice + ) internal { + (uint64 expiryTime, uint256 id) = _rnsUnified.mint(LibRNSDomain.RON_ID, name, resolver, owner, duration); + if (data.length != 0) _setRecords(resolver, id, data); + if (reverseRecord) _setReverseRecord(name, owner); + emit NameRegistered(name, id, owner, ronPrice, usdPrice, expiryTime); + } + + /** + * @dev Helper method to update RNSDomainPrice contract. + * + * Emits an event {DomainPriceUpdated}. + */ + function _setPriceOracle(INSDomainPrice priceOracle) internal { + _priceOracle = priceOracle; + emit DomainPriceUpdated(_msgSender(), priceOracle); + } +} 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/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..890fdb57 --- /dev/null +++ b/src/interfaces/INSDomainPrice.sol @@ -0,0 +1,200 @@ +//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 RenewalReservationRatioUpdated(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 renewal reservation ratio. + */ + function getRenewalReservationRatio() external view returns (uint256 rnfReservationRatio); + + /** + * @dev Sets renewal reservation ratio. + * + * Requirements: + * - The method caller is admin. + * + * Emits an event {RenewalReservationRatioUpdated}. + */ + function setRenewalReservationRatio(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 view returns (bytes32); + + /** + * @dev Max percentage 100%. Values [0; 100_00] reflexes [0; 100%] + */ + function MAX_PERCENTAGE() external view returns (uint64); +} diff --git a/src/interfaces/INSReverseRegistrar.sol b/src/interfaces/INSReverseRegistrar.sol new file mode 100644 index 00000000..e0497449 --- /dev/null +++ b/src/interfaces/INSReverseRegistrar.sol @@ -0,0 +1,96 @@ +// SPDX-LicINSe-Identifier: UNLICINSED +pragma solidity ^0.8.0; + +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import { INameResolver } from "./resolvers/INameResolver.sol"; +import { INSUnified } from "./INSUnified.sol"; + +/// @dev See https://eips.ethereum.org/EIPS/eip-181#registrar +interface IERC181 { + /** + * @dev Claims the name hex(addr) + '.addr.reverse' for addr. + * + * @param addr The address to set as the addr of the reverse record in INS. + * @return id The INS node hash of the reverse record. + */ + function claim(address addr) external returns (uint256 id); + + /** + * @dev Claims the name hex(owner) + '.addr.reverse' for owner and sets resolver. + * + * @param addr The address to set as the owner of the reverse record in INS. + * @param resolver The address of the resolver to set; 0 to leave unchanged. + * @return id The INS node hash of the reverse record. + */ + function claimWithResolver(address addr, address resolver) external returns (uint256 id); + + /** + * @dev Sets the name record for the reverse INS record associated with the calling account. First updates the + * resolver to the default reverse resolver if necessary. + * + * @param name The name to set for this address. + * @return The INS node hash of the reverse record. + */ + function setName(string memory name) external returns (uint256); +} + +interface INSReverseRegistrar is IERC181, IERC165 { + /// @dev Error: The provided id is not child node of `ADDR_REVERSE_ID` + error InvalidId(); + /// @dev Error: The contract is not authorized for minting or modifying domain hex(addr) + '.addr.reverse'. + error InvalidConfig(); + /// @dev Error: The sender lacks the necessary permissions. + error Unauthorized(); + /// @dev Error: The provided resolver address is null. + error NullAssignment(); + + /// @dev Emitted when reverse node is claimed. + event ReverseClaimed(address indexed addr, uint256 indexed id); + /// @dev Emitted when the default resolver is changed. + event DefaultResolverChanged(INameResolver indexed resolver); + + /** + * @dev Returns the controller role. + */ + function CONTROLLER_ROLE() external pure returns (bytes32); + + /** + * @dev Returns default resolver. + */ + function getDefaultResolver() external view returns (INameResolver); + + /** + * @dev Returns RNSUnified contract. + */ + function getRNSUnified() external view returns (INSUnified); + + /** + * @dev Sets default resolver. + * + * Requirement: + * + * - The method caller must be admin. + * + * Emitted an event {DefaultResolverChanged}. + * + */ + function setDefaultResolver(INameResolver resolver) external; + + /** + * @dev Same as {IERC181-setName}. + */ + function setNameForAddr(address addr, string memory name) external returns (uint256 id); + + /** + * @dev Returns address that the reverse node resolves for. + * Eg. node namehash('{addr}.addr.reverse') will always resolve for `addr`. + */ + function getAddress(uint256 id) external view returns (address); + + /** + * @dev Returns the id hash for a given account's reverse records. + * @param addr The address to hash + * @return The INS node hash. + */ + function computeId(address addr) external pure returns (uint256); +} diff --git a/src/interfaces/INSUnified.sol b/src/interfaces/INSUnified.sol index 0863335f..17f5a646 100644 --- a/src/interfaces/INSUnified.sol +++ b/src/interfaces/INSUnified.sol @@ -209,7 +209,7 @@ interface INSUnified is IAccessControlEnumerable, IERC721Metadata { * * Emits an event {RecordUpdated}. */ - function renew(uint256 id, uint64 duration) external; + function renew(uint256 id, uint64 duration) external returns (uint64 expiry); /** * @dev Sets expiry time for a token. Update operation for {Record.mut.expiry}. diff --git a/src/interfaces/INameChecker.sol b/src/interfaces/INameChecker.sol new file mode 100644 index 00000000..63d2d191 --- /dev/null +++ b/src/interfaces/INameChecker.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/** + * @title INameChecker + * @dev The INameChecker interface provides functions for managing and checking substrings and forbidden words in strings. + */ +interface INameChecker { + /** + * @dev Emitted when the word range is updated. + * @param operator The address that updated the word range. + * @param min The minimum word length allowed. + * @param max The maximum word length allowed. + */ + event WordRangeUpdated(address indexed operator, uint8 min, uint8 max); + + /** + * @dev Emitted when the forbidden words are updated. + * @param operator The address that updated the forbidden words list. + * @param wordCount The number of words in the list. + * @param shouldForbid Boolean indicating whether the specified words should be forbidden. + */ + event ForbiddenWordsUpdated(address indexed operator, uint256 wordCount, bool shouldForbid); + + /** + * @dev Returns an array of all substrings of a given string. + * @param str The input string to analyze. + * @return subStrings An array of all substrings. + */ + function getAllSubStrings(string calldata str) external view returns (string[] memory subStrings); + + /** + * @dev Returns the total number of substrings for a given string length, as well as the minimum and maximum allowed word lengths. + * @param strlen The length of the input string. + * @return total The total number of substrings. + * @return min The minimum word length allowed. + * @return max The maximum word length allowed. + */ + function totalSubString(uint256 strlen) external view returns (uint256 total, uint256 min, uint256 max); + + /** + * @dev Sets a list of forbidden words and specifies whether they should be forbidden. + * @param packedWords An array of packed word representations. + * @param shouldForbid Boolean indicating whether the specified words should be forbidden. + */ + function setForbiddenWords(uint256[] calldata packedWords, bool shouldForbid) external; + + /** + * @dev Sets a list of forbidden words and specifies whether they should be forbidden. + * @param words An array of raw words in string representations. + * @param shouldForbid Boolean indicating whether the specified words should be forbidden. + */ + function setForbiddenWords(string[] calldata words, bool shouldForbid) external; + + /** + * @dev Sets the minimum and maximum word lengths allowed. + * @param min The minimum word length. + * @param max The maximum word length. + */ + function setWordRange(uint8 min, uint8 max) external; + + /** + * @dev Retrieves the current minimum and maximum word lengths allowed. + * @return min The minimum word length allowed. + * @return max The maximum word length allowed. + */ + function getWordRange() external view returns (uint8 min, uint8 max); + + /** + * @notice Checks if a given name contains any forbidden characters or blacklisted words. + * @param name The string to check. + * @return true if the name contains forbidden characters or blacklisted words, false otherwise. + */ + function forbidden(string calldata name) external view returns (bool); + + /** + * @notice Checks if a given name contains any blacklisted words. + * @param name The string to check. + * @return true if the name contains blacklisted words, false otherwise. + */ + function containsBlacklistedWord(string calldata name) external view returns (bool); + + /** + * @notice Checks if a given name contains any invalid characters. + * requirements: + * - all characters in name must in range [a-z] or [0-9]. + * @param name The string to check. + * @return true if the name contains invalid characters, false otherwise. + */ + function containsInvalidCharacter(string calldata name) external pure returns (bool); + + /** + * @dev Packs a string into a single word representation. + * @param str The string to be packed. + * @notice Returns `uint256(0)` if the length is zero or greater than 31. + * @return packed The packed value of the input string. + */ + function pack(string memory str) external pure returns (uint256 packed); + + /** + * @dev Packs an array of strings into their single word representations. + * @param strs The array of strings to be packed. + * @notice Returns an array of packed values, along with the minimum and maximum string lengths. + * @return packeds An array containing the packed values of the input strings. + */ + function packBulk(string[] memory strs) external pure returns (uint256[] memory packeds); +} diff --git a/src/interfaces/IRONRegistrarController.sol b/src/interfaces/IRONRegistrarController.sol new file mode 100644 index 00000000..74799ba4 --- /dev/null +++ b/src/interfaces/IRONRegistrarController.sol @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { INSUnified } from "./INSUnified.sol"; +import { INSDomainPrice } from "./INSDomainPrice.sol"; +import { INameChecker } from "./INameChecker.sol"; +import { INSReverseRegistrar } from "./INSReverseRegistrar.sol"; + +/** + * @title IRONRegistrarController + * @dev Interface for the Registrar Controller contract that manages the registration, renewal, and commitment of RNS + * names. + */ +interface IRONRegistrarController { + /// @dev Error: The provided commitment timestamp is too new for registration. + error CommitmentTooNew(bytes32 commitment); + /// @dev Error: The provided commitment timestamp is too old for registration. + error CommitmentTooOld(bytes32 commitment); + /// @dev Error: The requested name is not available for registration. + error NameNotAvailable(string name); + /// @dev Error: The requested duration for registration is too short. + error DurationTooShort(uint64 duration); + /// @dev Error: A resolver is required when additional data is supplied during registration. + error ResolverRequiredWhenDataSupplied(); + /// @dev Error: An unexpired commitment already exists for the given commitment. + error UnexpiredCommitmentExists(bytes32 commitment); + /// @dev Error: Insufficient value (RON) provided for registration. + error InsufficientValue(); + /// @dev Error: The sender is not authorized for the given RNS node. + error Unauthorized(bytes32 node); + /// @dev Error: The maximum commitment age is set too low. + error MaxCommitmentAgeTooLow(); + /// @dev Error: The maximum commitment age is set too high. + error MaxCommitmentAgeTooHigh(); + /// @dev Thrown when some one requests for protected names + error ErrRequestedForProtectedName(string name); + /// @dev Thrown when received invalid params for registering protected name + error ErrInvalidRegisterProtectedName(string name, address requestOwner, bool nameProtected, bool ownerWhitelisted); + /// @dev Thrown when received invalid array length + error InvalidArrayLength(); + + /** + * @dev Emitted when the min registration duration is updated. + * @param operator The address of the operator who triggered the update. + * @param duration The new duration in seconds. + */ + event MinRegistrationDurationUpdated(address indexed operator, uint256 duration); + + /** + * @dev Emitted when RNSDomainPrice contract is updated. + * @param operator The address of the operator who triggered the update. + * @param newDomainPrice The new duration domain price contract. + */ + event DomainPriceUpdated(address indexed operator, INSDomainPrice newDomainPrice); + + /** + * @dev Emitted when the commitment age range is updated. + * @param operator The address of the operator who triggered the update. + * @param minCommitmentAge The new minimum commitment age in seconds. + * @param maxCommitmentAge The new maximum commitment age in seconds. + */ + event CommitmentAgeUpdated(address indexed operator, uint256 minCommitmentAge, uint256 maxCommitmentAge); + + /** + * @dev Emitted when a new name is successfully registered. + * @param name The registered name. + * @param id The namehash of the registered name. + * @param owner The owner of the registered name. + * @param ronPrice The cost of the registration in RON. + * @param usdPrice The cost of the registration in USD. + * @param expires The expiration timestamp of the registration. + */ + event NameRegistered( + string name, uint256 indexed id, address indexed owner, uint256 ronPrice, uint256 usdPrice, uint64 expires + ); + + /** + * @dev Emitted when a name is renewed. + * @param name The renewed name. + * @param id The namehash of the registered name. + * @param cost The cost of renewal. + * @param expires The new expiration timestamp after renewal. + */ + event NameRenewed(string name, uint256 indexed id, uint256 cost, uint64 expires); + + /** + * @dev Emitted the whitelist status is updated for the owners of the protected names. + * @param operator The address of the operator who triggered the update. + */ + event ProtectedNamesWhitelisted(address indexed operator, uint256[] ids, address[] owners, bool status); + + /** + * @dev Retrieves the rent price for a given name and duration. + * @param name The name for which to calculate the rent price. + * @param duration The duration of the rent. + * @return usdPrice rent price in usd. + * @return ronPrice rent price in ron. + */ + function rentPrice(string memory name, uint64 duration) external view returns (uint256 usdPrice, uint256 ronPrice); + + /** + * @dev Calculate the corresponding id given RON_ID and name. + */ + function computeId(string memory name) external pure returns (uint256 id); + + /** + * @dev Checks if a name is valid. + * @param name The name to check validity for. + * @return A boolean indicating whether the name is available. + */ + function valid(string memory name) external view returns (bool); + + /** + * @dev Checks if a name is available for registration. + * @param name The name to check availability for. + * @return A boolean indicating whether the name is available. + */ + function available(string memory name) external returns (bool); + + /** + * @dev Generates the commitment hash for a registration. + * @param name The name to be registered. + * @param owner The owner of the name. + * @param duration The duration of the registration. + * @param secret The secret used for the commitment. + * @param resolver The resolver contract address. + * @param data Additional data associated with the registration. + * @param reverseRecord Whether to use reverse record for additional data. + * @return The commitment hash. + */ + function computeCommitment( + string memory name, + address owner, + uint64 duration, + bytes32 secret, + address resolver, + bytes[] calldata data, + bool reverseRecord + ) external pure returns (bytes32); + + /** + * @dev Commits to a registration using the commitment hash. + * @param commitment The commitment hash. + */ + function commit(bytes32 commitment) external; + + /** + * @dev Registers a new name. + * @param name The name to be registered. + * @param owner The owner of the name. + * @param duration The duration of the registration. + * @param secret The secret used for the commitment. + * @param resolver The resolver contract address. + * @param data Additional data associated with the registration. + * @param reverseRecord Whether to use reverse record for additional data. + */ + function register( + string calldata name, + address owner, + uint64 duration, + bytes32 secret, + address resolver, + bytes[] calldata data, + bool reverseRecord + ) external payable; + + /** + * @dev Renews an existing name registration. + * @param name The name to be renewed. + * @param duration The duration of the renewal. + */ + function renew(string calldata name, uint64 duration) external payable; + + /** + * @dev Registers a protected name. + * + * Requirements: + * - The owner is whitelisted for registering. + */ + function registerProtectedName( + string memory name, + address owner, + uint64 duration, + address resolver, + bytes[] calldata data, + bool reverseRecord + ) external payable; + + /** + * @dev Updates min registration duration. + * + * Requirements: + * - The caller must have the admin role. + * + */ + function setMinRegistrationDuration(uint256 duration) external; + + /** + * @dev Sets the minimum and maximum commitment ages. + * + * Requirements: + * - Caller must have the DEFAULT_ADMIN_ROLE. + * - The `maxCommitmentAge` must be less than or equal to the current block timestamp. + * - The `maxCommitmentAge` must be greater than the `minCommitmentAge`. + * + * Emits a {CommitmentAgeUpdated} event indicating the successful update of the age range. + * + * @param minCommitmentAge The minimum commitment age in seconds. + * @param maxCommitmentAge The maximum commitment age in seconds. + */ + function setCommitmentAge(uint256 minCommitmentAge, uint256 maxCommitmentAge) external; + + /** + * @dev Bulk (de)whitelist for buying protected names. + * + * Requirements: + * - The method caller is contract operator. + * + * Emits an event {ProtectedNamesWhitelisted}. + */ + function bulkWhitelistProtectedNames(uint256[] calldata ids, address[] calldata owners, bool status) external; + + /** + * @dev Returns the whitelist status for registering protected name. + */ + function getWhitelistProtectedNameStatus(uint256 id, address owner) external view returns (bool status); + + /** + * @dev Updates treasury address. + * + * Requirements: + * - The caller must have the admin role. + * + */ + function setTreasury(address payable) external; + + /** + * @dev Updates price oracle address. + * + * Requirements: + * - The caller must have the admin role. + */ + function setPriceOracle(INSDomainPrice) external; + + /** + * @dev Returns the treasury address. + */ + function getTreasury() external view returns (address); + + /** + * @dev Pauses the registrar controller's functionality. + * + * Requirements: + * - The caller must have the admin role. + * + */ + function pause() external; + + /** + * @dev Unpauses the registrar controller's functionality. + * + * Requirements: + * - The caller must have the admin role. + * + */ + function unpause() external; + + /** + * @dev Returns the role identifier for the pauser role. + */ + function PAUSER_ROLE() external pure returns (bytes32); + + /** + * @dev Returns the operator role. + */ + function OPERATOR_ROLE() external pure returns (bytes32); + + /** + * @dev Returns the threshold for valid name length. + */ + function MIN_DOMAIN_LENGTH() external view returns (uint8); + + /** + * @dev Returns the minimum registration duration. + */ + function getMinRegistrationDuration() external view returns (uint256); + + /** + * @dev Returns the range of commitment ages allowed. + */ + function getCommitmentAgeRange() external view returns (uint256 minCommitmentAge, uint256 maxCommitmentAge); + + /** + * @dev Returns the INSUnified contract associated with this controller. + */ + function getRNSUnified() external view returns (INSUnified); + + /** + * @dev Returns the INSDomainPrice contract associated with this controller. + */ + function getPriceOracle() external view returns (INSDomainPrice); + + /** + * @dev Returns the INameChecker contract associated with this controller. + */ + function getNameChecker() external view returns (INameChecker); + + /** + * @dev Returns the IReverseRegistrar contract associated with this controller. + */ + function getReverseRegistrar() external view returns (INSReverseRegistrar); +} 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..20f240cc --- /dev/null +++ b/src/interfaces/resolvers/IPublicResolver.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { INSUnified } from "../INSUnified.sol"; +import { INSReverseRegistrar } from "../INSReverseRegistrar.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 (INSReverseRegistrar); + + /** + * @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/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..f86610e3 --- /dev/null +++ b/src/libraries/LibRNSDomain.sol @@ -0,0 +1,58 @@ +//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 len; + uint256 i = 0; + uint256 bytelength = bytes(s).length; + for (len = 0; 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/LibSubString.sol b/src/libraries/LibSubString.sol new file mode 100644 index 00000000..b7990ca9 --- /dev/null +++ b/src/libraries/LibSubString.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + +/** + * @title LibSubString + * @dev A library for working with substrings and word ranges in strings. + */ +library LibSubString { + error TotalSubStringTooLarge(uint256 total); + /** + * @dev Struct representing a word range with minimum and maximum word lengths. + */ + struct WordRange { + uint8 min; + uint8 max; + } + + uint256 public constant MAX_SUBSTRING_SIZE = type(uint16).max; + + /** + * @dev Retrieves all possible substrings within a given string based on a specified word range. + * @param str The input string to analyze. + * @param wordRange The word range specifying the minimum and maximum word lengths. + * @return subStrings An array of all possible substrings within the input string. + */ + function getAllSubStrings(string calldata str, WordRange memory wordRange) + internal + pure + returns (string[] memory subStrings) + { + unchecked { + uint256 length = bytes(str).length; + (uint256 total, uint256 min, uint256 max) = totalSubString(length, wordRange); + subStrings = new string[](total); + uint256 idx; + uint256 bLength; + + for (uint256 i; i < length; ++i) { + bLength = Math.min(i + max, length); + + for (uint256 j = i + min; j <= bLength; ++j) { + subStrings[idx++] = str[i:j]; + } + } + } + } + + /** + * @dev Calculates the total number of possible substrings within a given string length based on a specified word range. + * @param len The length of the input string. + * @param wordRange The word range specifying the minimum and maximum word lengths. + * @return total The total number of possible substrings. + * @return min The minimum word length allowed. + * @return max The maximum word length allowed. + */ + function totalSubString(uint256 len, WordRange memory wordRange) + internal + pure + returns (uint256 total, uint256 min, uint256 max) + { + unchecked { + min = Math.min(wordRange.min, len); + max = Math.min(wordRange.max, len); + uint256 range = max - min; + // `(range + 1)` represents the number of possible substring lengths in `range`. + // `(strlen - min + 1)` represents the number of possible starting positions for substrings with a minimum length of `min`. + total = (range + 1) * (len - min + 1) - (((range + 1) * range) >> 1); + if (total > MAX_SUBSTRING_SIZE) revert TotalSubStringTooLarge(total); + } + } +} 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/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)); + } +}