From d0d536a986c09e87eb2126c5398518103ce586ff Mon Sep 17 00:00:00 2001 From: Tu Do <18521578@gm.uit.edu.vn> Date: Thu, 12 Oct 2023 13:08:22 +0700 Subject: [PATCH] feat: pull latest code from branch release to resolve conflict (#22) --- src/RNSReverseRegistrar.sol | 166 ++++++++++ src/RNSUnified.sol | 151 +++++---- src/interfaces/INSUnified.sol | 66 ++-- src/interfaces/IReverseRegistrar.sol | 95 +++++- src/libraries/LibModifyingField.sol | 1 - src/libraries/LibStrAddrConvert.sol | 60 ++++ src/resolvers/PublicResolver.sol | 6 +- src/types/ModifyingIndicator.sol | 26 +- test/RNSUnified/RNSUnified.ERC721.t.sol | 67 ++++ .../RNSUnified.bulkSetProtected.t.sol | 71 +++++ test/RNSUnified/RNSUnified.mint.t.sol | 88 ++++++ test/RNSUnified/RNSUnified.reclaim.t.sol | 51 +++ test/RNSUnified/RNSUnified.setExpiry.t.sol | 96 ++++++ test/RNSUnified/RNSUnified.setRecord.t.sol | 127 ++++++++ test/RNSUnified/RNSUnified.t.sol | 291 ++++++++++++++++++ test/libraries/LibStrAddrConvert.t.sol | 32 ++ 16 files changed, 1295 insertions(+), 99 deletions(-) create mode 100644 src/RNSReverseRegistrar.sol create mode 100644 src/libraries/LibStrAddrConvert.sol create mode 100644 test/RNSUnified/RNSUnified.ERC721.t.sol create mode 100644 test/RNSUnified/RNSUnified.bulkSetProtected.t.sol create mode 100644 test/RNSUnified/RNSUnified.mint.t.sol create mode 100644 test/RNSUnified/RNSUnified.reclaim.t.sol create mode 100644 test/RNSUnified/RNSUnified.setExpiry.t.sol create mode 100644 test/RNSUnified/RNSUnified.setRecord.t.sol create mode 100644 test/RNSUnified/RNSUnified.t.sol create mode 100644 test/libraries/LibStrAddrConvert.t.sol diff --git a/src/RNSReverseRegistrar.sol b/src/RNSReverseRegistrar.sol new file mode 100644 index 00000000..2a443591 --- /dev/null +++ b/src/RNSReverseRegistrar.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { INameResolver } from "@rns-contracts/interfaces/resolvers/INameResolver.sol"; +import { IERC165, IERC181, IReverseRegistrar } from "@rns-contracts/interfaces/IReverseRegistrar.sol"; +import { INSUnified } from "@rns-contracts/interfaces/INSUnified.sol"; +import { LibStrAddrConvert } from "@rns-contracts/libraries/LibStrAddrConvert.sol"; + +/** + * @notice Customized version of ReverseRegistrar: https://github.com/ensdomains/ens-contracts/blob/0c75ba23fae76165d51c9c80d76d22261e06179d/contracts/reverseRegistrar/ReverseRegistrar.sol + * @dev The reverse registrar provides functions to claim a reverse record, as well as a convenience function to + * configure the record as it's most commonly used, as a way of specifying a canonical name for an address. + * The reverse registrar is specified in EIP 181 https://eips.ethereum.org/EIPS/eip-181. + */ +contract RNSReverseRegistrar is Initializable, Ownable, IReverseRegistrar { + /// @dev This controller must equal to IReverseRegistrar.CONTROLLER_ROLE() + bytes32 public constant CONTROLLER_ROLE = keccak256("CONTROLLER_ROLE"); + /// @dev Value equals to namehash('addr.reverse') + bytes32 public constant ADDR_REVERSE_NODE = 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2; + + /// @dev Gap for upgradeability. + uint256[50] private ____gap; + /// @dev The rns unified contract. + INSUnified internal _rnsUnified; + /// @dev The default resolver. + INameResolver internal _defaultResolver; + + modifier live() { + _requireLive(); + _; + } + + modifier onlyAuthorized(address addr) { + _requireAuthorized(addr); + _; + } + + constructor() payable { + _disableInitializers(); + } + + function initialize(address admin, INSUnified rnsUnified) external initializer { + _rnsUnified = rnsUnified; + _transferOwnership(admin); + } + + /** + * @inheritdoc IReverseRegistrar + */ + function getDefaultResolver() external view returns (INameResolver) { + return _defaultResolver; + } + + /** + * @inheritdoc IReverseRegistrar + */ + function getRNSUnified() external view returns (INSUnified) { + return _rnsUnified; + } + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) external pure returns (bool) { + return interfaceId == type(IReverseRegistrar).interfaceId || interfaceId == type(IERC165).interfaceId + || interfaceId == type(IERC181).interfaceId; + } + + /** + * @inheritdoc IReverseRegistrar + */ + function setDefaultResolver(INameResolver resolver) external onlyOwner { + if (address(resolver) == address(0)) revert NullAssignment(); + _defaultResolver = resolver; + emit DefaultResolverChanged(resolver); + } + + /** + * @inheritdoc IERC181 + */ + function claim(address addr) external returns (bytes32) { + return claimWithResolver(addr, address(_defaultResolver)); + } + + /** + * @inheritdoc IERC181 + */ + function setName(string memory name) external returns (bytes32 node) { + return setNameForAddr(_msgSender(), name); + } + + /** + * @inheritdoc IReverseRegistrar + */ + function getAddress(bytes32 node) external view returns (address) { + INSUnified.Record memory record = _rnsUnified.getRecord(uint256(node)); + if (record.immut.parentId != uint256(ADDR_REVERSE_NODE)) revert InvalidNode(); + return LibStrAddrConvert.parseAddr(record.immut.label); + } + + /** + * @inheritdoc IERC181 + */ + function claimWithResolver(address addr, address resolver) public live onlyAuthorized(addr) returns (bytes32 node) { + node = _claimWithResolver(addr, resolver); + } + + /** + * @inheritdoc IReverseRegistrar + */ + function setNameForAddr(address addr, string memory name) + public + live + onlyAuthorized(addr) + returns (bytes32 node) + { + node = computeNode(addr); + INSUnified rnsUnified = _rnsUnified; + if (rnsUnified.ownerOf(uint256(node)) != address(this)) { + bytes32 claimedNode = _claimWithResolver(addr, address(_defaultResolver)); + if (claimedNode != node) revert InvalidNode(); + } + + INSUnified.Record memory record = rnsUnified.getRecord(uint256(node)); + INameResolver(record.mut.resolver).setName(node, name); + } + + /** + * @inheritdoc IReverseRegistrar + */ + function computeNode(address addr) public pure returns (bytes32) { + return keccak256(abi.encodePacked(ADDR_REVERSE_NODE, keccak256(bytes(LibStrAddrConvert.toString(addr))))); + } + + /** + * @dev Helper method to claim domain hex(addr) + '.addr.reverse' for addr. + * Emits an event {ReverseClaimed}. + */ + function _claimWithResolver(address addr, address resolver) internal returns (bytes32 node) { + string memory stringifiedAddr = LibStrAddrConvert.toString(addr); + (, uint256 id) = + _rnsUnified.mint(uint256(ADDR_REVERSE_NODE), stringifiedAddr, resolver, address(this), type(uint64).max); + node = bytes32(id); + emit ReverseClaimed(addr, node); + } + + /** + * @dev Helper method to ensure the contract can mint or modify domain hex(addr) + '.addr.reverse' for addr. + */ + function _requireLive() internal view { + if (_rnsUnified.ownerOf(uint256(ADDR_REVERSE_NODE)) == address(this)) revert InvalidConfig(); + } + + /** + * @dev Helper method to ensure addr is authorized for claiming domain hex(addr) + '.addr.reverse' for addr. + */ + function _requireAuthorized(address addr) internal view { + address sender = _msgSender(); + INSUnified rnsUnified = _rnsUnified; + if (!(addr == sender || rnsUnified.hasRole(CONTROLLER_ROLE, sender) || rnsUnified.isApprovedForAll(addr, sender))) { + revert Unauthorized(); + } + } +} diff --git a/src/RNSUnified.sol b/src/RNSUnified.sol index d77b5bf6..beb7b1dd 100644 --- a/src/RNSUnified.sol +++ b/src/RNSUnified.sol @@ -5,7 +5,12 @@ import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable import { IERC721State, IERC721, ERC721, INSUnified, RNSToken } from "./RNSToken.sol"; import { LibSafeRange } from "./libraries/math/LibSafeRange.sol"; import { ModifyingField, LibModifyingField } from "./libraries/LibModifyingField.sol"; -import { ALL_FIELDS_INDICATOR, IMMUTABLE_FIELDS_INDICATOR, ModifyingIndicator } from "./types/ModifyingIndicator.sol"; +import { + ALL_FIELDS_INDICATOR, + IMMUTABLE_FIELDS_INDICATOR, + USER_FIELDS_INDICATOR, + ModifyingIndicator +} from "./types/ModifyingIndicator.sol"; contract RNSUnified is Initializable, RNSToken { using LibModifyingField for ModifyingField; @@ -19,7 +24,7 @@ contract RNSUnified is Initializable, RNSToken { uint256[50] private ____gap; uint64 internal _gracePeriod; - /// @dev Mapping from token id => records + /// @dev Mapping from token id => record mapping(uint256 => Record) internal _recordOf; modifier onlyAuthorized(uint256 id, ModifyingIndicator indicator) { @@ -47,7 +52,10 @@ contract RNSUnified is Initializable, RNSToken { _setBaseURI(baseTokenURI); _setGracePeriod(gracePeriod); - _mint(admin, 0x00); + _mint(admin, 0x0); + Record memory record; + _recordOf[0x0].mut.expiry = record.mut.expiry = MAX_EXPIRY; + emit RecordUpdated(0x0, ModifyingField.Expiry.indicator(), record); } /// @inheritdoc INSUnified @@ -71,7 +79,7 @@ contract RNSUnified is Initializable, RNSToken { } /// @inheritdoc INSUnified - function mint(uint256 parentId, string calldata label, address resolver, uint64 ttl, address owner, uint64 duration) + function mint(uint256 parentId, string calldata label, address resolver, address owner, uint64 duration) external whenNotPaused returns (uint64 expiryTime, uint256 id) @@ -86,19 +94,33 @@ contract RNSUnified is Initializable, RNSToken { _mint(owner, id); expiryTime = uint64(LibSafeRange.addWithUpperbound(block.timestamp, duration, MAX_EXPIRY)); + _requireValidExpiry(parentId, expiryTime); Record memory record; - record.mut = MutableRecord({ resolver: resolver, ttl: ttl, owner: owner, expiry: expiryTime, protected: false }); + record.mut = MutableRecord({ resolver: resolver, owner: owner, expiry: expiryTime, protected: false }); record.immut = ImmutableRecord({ depth: _recordOf[parentId].immut.depth + 1, parentId: parentId, label: label }); _recordOf[id] = record; - emit RecordsUpdated(id, ALL_FIELDS_INDICATOR, record); + emit RecordUpdated(id, ALL_FIELDS_INDICATOR, record); + } + + /// @inheritdoc INSUnified + function getRecord(uint256 id) external view returns (Record memory record) { + record = _recordOf[id]; + record.mut.expiry = _expiry(id); } /// @inheritdoc INSUnified - function getRecords(uint256 id) external view returns (Record memory records, string memory domain) { - records = _recordOf[id]; - records.mut.expiry = _expiry(id); - domain = _getDomain(records.immut.parentId, records.immut.label); + function getDomain(uint256 id) external view returns (string memory domain) { + if (id == 0) return ""; + + ImmutableRecord storage sRecord = _recordOf[id].immut; + domain = sRecord.label; + id = sRecord.parentId; + while (id != 0) { + sRecord = _recordOf[id].immut; + domain = string.concat(domain, ".", sRecord.label); + id = sRecord.parentId; + } } /// @inheritdoc INSUnified @@ -112,12 +134,17 @@ contract RNSUnified is Initializable, RNSToken { /// @inheritdoc INSUnified function renew(uint256 id, uint64 duration) external whenNotPaused onlyRole(CONTROLLER_ROLE) { - _setExpiry(id, uint64(LibSafeRange.addWithUpperbound(_recordOf[id].mut.expiry, duration, MAX_EXPIRY))); + Record memory record; + record.mut.expiry = uint64(LibSafeRange.addWithUpperbound(_recordOf[id].mut.expiry, duration, MAX_EXPIRY)); + _setExpiry(id, record.mut.expiry); + emit RecordUpdated(id, ModifyingField.Expiry.indicator(), record); } /// @inheritdoc INSUnified function setExpiry(uint256 id, uint64 expiry) external whenNotPaused onlyRole(CONTROLLER_ROLE) { - _setExpiry(id, expiry); + Record memory record; + _setExpiry(id, record.mut.expiry = expiry); + emit RecordUpdated(id, ModifyingField.Expiry.indicator(), record); } /// @inheritdoc INSUnified @@ -129,9 +156,10 @@ contract RNSUnified is Initializable, RNSToken { for (uint256 i; i < ids.length;) { id = ids[i]; + if (!_exists(id)) revert Unexists(); if (_recordOf[id].mut.protected != protected) { _recordOf[id].mut.protected = protected; - emit RecordsUpdated(id, indicator, record); + emit RecordUpdated(id, indicator, record); } unchecked { @@ -141,14 +169,29 @@ contract RNSUnified is Initializable, RNSToken { } /// @inheritdoc INSUnified - function setRecords(uint256 id, ModifyingIndicator indicator, MutableRecord calldata mutRecord) + function setRecord(uint256 id, ModifyingIndicator indicator, MutableRecord calldata mutRecord) external whenNotPaused onlyAuthorized(id, indicator) { Record memory record; - _recordOf[id].mut = record.mut = mutRecord; - emit RecordsUpdated(id, indicator, record); + MutableRecord storage sMutRecord = _recordOf[id].mut; + + if (indicator.hasAny(ModifyingField.Protected.indicator())) { + sMutRecord.protected = record.mut.protected = mutRecord.protected; + } + if (indicator.hasAny(ModifyingField.Expiry.indicator())) { + _setExpiry(id, record.mut.expiry = mutRecord.expiry); + } + if (indicator.hasAny(ModifyingField.Resolver.indicator())) { + sMutRecord.resolver = record.mut.resolver = mutRecord.resolver; + } + emit RecordUpdated(id, indicator, record); + + // Updating owner might emit more {RecordUpdated} events. See method {_transfer}. + if (indicator.hasAny(ModifyingField.Owner.indicator())) { + _safeTransfer(_recordOf[id].mut.owner, mutRecord.owner, id, ""); + } } /** @@ -159,7 +202,7 @@ contract RNSUnified is Initializable, RNSToken { } /// @inheritdoc INSUnified - function canSetRecords(address requester, uint256 id, ModifyingIndicator indicator) + function canSetRecord(address requester, uint256 id, ModifyingIndicator indicator) public view returns (bool allowed, bytes4) @@ -167,6 +210,7 @@ contract RNSUnified is Initializable, RNSToken { if (indicator.hasAny(IMMUTABLE_FIELDS_INDICATOR)) { return (false, CannotSetImmutableField.selector); } + if (!_exists(id)) return (false, Unexists.selector); if (indicator.hasAny(ModifyingField.Protected.indicator()) && !hasRole(PROTECTED_SETTLER_ROLE, requester)) { return (false, MissingProtectedSettlerRole.selector); } @@ -174,20 +218,16 @@ contract RNSUnified is Initializable, RNSToken { if (indicator.hasAny(ModifyingField.Expiry.indicator()) && !hasControllerRole) { return (false, MissingControllerRole.selector); } - if ( - indicator.hasAny( - ModifyingField.Resolver.indicator() | ModifyingField.Ttl.indicator() | ModifyingField.Owner.indicator() - ) && !(hasControllerRole || _checkOwnerRules(requester, id)) - ) { + if (indicator.hasAny(USER_FIELDS_INDICATOR) && !(hasControllerRole || _checkOwnerRules(requester, id))) { return (false, Unauthorized.selector); } - return (true, 0x00); + return (true, 0x0); } /// @dev Override {ERC721-ownerOf}. function ownerOf(uint256 tokenId) public view override(ERC721, IERC721) returns (address) { - if (_isExpired(tokenId)) return address(0); + if (_isExpired(tokenId)) return address(0x0); return super.ownerOf(tokenId); } @@ -236,29 +276,23 @@ contract RNSUnified is Initializable, RNSToken { } /** - * @dev Helper method to ensure msg.sender is authorized to modify records of the token id. + * @dev Helper method to ensure msg.sender is authorized to modify record of the token id. */ function _requireAuthorized(uint256 id, ModifyingIndicator indicator) internal view { - (bool allowed, bytes4 errorCode) = canSetRecords(_msgSender(), id, indicator); + (bool allowed, bytes4 errorCode) = canSetRecord(_msgSender(), id, indicator); if (!allowed) { assembly ("memory-safe") { - mstore(0x00, errorCode) - revert(0x1c, 0x04) + mstore(0x0, errorCode) + revert(0x0, 0x04) } } } /** - * @dev Helper method to get full domain name from parent id and current label. + * @dev Helper method to ensure expiry of an id is lower or equal expiry of parent id. */ - function _getDomain(uint256 parentId, string memory label) internal view returns (string memory domain) { - if (parentId == 0) return ""; - domain = label; - - while (parentId != 0) { - domain = string.concat(domain, ".", _recordOf[parentId].immut.label); - parentId = _recordOf[parentId].immut.parentId; - } + function _requireValidExpiry(uint256 parentId, uint64 expiry) internal view { + if (expiry > _recordOf[parentId].mut.expiry) revert ExceedParentExpiry(); } /** @@ -268,17 +302,15 @@ contract RNSUnified is Initializable, RNSToken { * - The token must be registered or in grace period. * - Expiry time must be larger than the old one. * - * Emits an event {RecordsUpdated}. + * Emits an event {RecordUpdated}. */ function _setExpiry(uint256 id, uint64 expiry) internal { + _requireValidExpiry(_recordOf[id].immut.parentId, expiry); if (available(id)) revert NameMustBeRegisteredOrInGracePeriod(); - if (expiry <= _recordOf[id].mut.expiry) { - revert ExpiryTimeMustBeLargerThanTheOldOne(); - } - Record memory record; + if (expiry <= _recordOf[id].mut.expiry) revert ExpiryTimeMustBeLargerThanTheOldOne(); + Record memory record; _recordOf[id].mut.expiry = record.mut.expiry = expiry; - emit RecordsUpdated(id, ModifyingField.Expiry.indicator(), record); } /** @@ -291,29 +323,24 @@ contract RNSUnified is Initializable, RNSToken { emit GracePeriodUpdated(_msgSender(), gracePeriod); } - /// @dev Override {ERC721-_afterTokenTransfer}. - function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) - internal - virtual - override - { - super._afterTokenTransfer(from, to, firstTokenId, batchSize); + /// @dev Override {ERC721-_transfer}. + function _transfer(address from, address to, uint256 id) internal override { + super._transfer(from, to, id); Record memory record; ModifyingIndicator indicator = ModifyingField.Owner.indicator(); - bool shouldUpdateProtected = !hasRole(PROTECTED_SETTLER_ROLE, _msgSender()); - if (shouldUpdateProtected) indicator = indicator | ModifyingField.Protected.indicator(); - - for (uint256 id = firstTokenId; id < firstTokenId + batchSize;) { - _recordOf[id].mut.owner = record.mut.owner = to; - if (shouldUpdateProtected) { - _recordOf[id].mut.protected = false; - emit RecordsUpdated(id, indicator, record); - } - unchecked { - id++; - } + _recordOf[id].mut.owner = record.mut.owner = to; + if (!hasRole(PROTECTED_SETTLER_ROLE, _msgSender()) && _recordOf[id].mut.protected) { + _recordOf[id].mut.protected = false; + indicator = indicator | ModifyingField.Protected.indicator(); } + emit RecordUpdated(id, indicator, record); + } + + /// @dev Override {ERC721-_burn}. + function _burn(uint256 id) internal override { + super._burn(id); + delete _recordOf[id].mut; } } diff --git a/src/interfaces/INSUnified.sol b/src/interfaces/INSUnified.sol index b62a4a68..98721522 100644 --- a/src/interfaces/INSUnified.sol +++ b/src/interfaces/INSUnified.sol @@ -2,11 +2,16 @@ pragma solidity ^0.8.19; import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import { IAccessControlEnumerable } from "@openzeppelin/contracts/access/IAccessControlEnumerable.sol"; import { ModifyingIndicator } from "../types/ModifyingIndicator.sol"; -interface INSUnified is IERC721Metadata { +interface INSUnified is IAccessControlEnumerable, IERC721Metadata { /// @dev Error: The provided token id is expired. error Expired(); + /// @dev Error: The provided token id is unexists. + error Unexists(); + /// @dev Error: The provided id expiry is greater than parent id expiry. + error ExceedParentExpiry(); /// @dev Error: The provided name is unavailable for registration. error Unavailable(); /// @dev Error: The sender lacks the necessary permissions. @@ -32,9 +37,9 @@ interface INSUnified is IERC721Metadata { struct ImmutableRecord { // The level-th of a domain. uint8 depth; - // The node of parent token. Eg, parent node of vip.dukethor.ron equals to namehash('dukethor.ron') + // The node of parent token. Eg, parent node of vip.duke.ron equals to namehash('duke.ron') uint256 parentId; - // The label of a domain. Eg, label is vip for domain vip.dukethor.ron + // The label of a domain. Eg, label is vip for domain vip.duke.ron string label; } @@ -42,18 +47,15 @@ interface INSUnified is IERC721Metadata { * | Fields\Idc,Roles | Modifying Indicator | Controller | Protected setter | (Parent) Owner/Spender | * | ---------------- | ------------------- | ---------- | ---------------- | ---------------------- | * | resolver | 0b00001000 | x | | x | - * | ttl | 0b00010000 | x | | x | - * | owner | 0b00100000 | x | | x | - * | expiry | 0b01000000 | x | | | - * | protected | 0b10000000 | | x | | + * | owner | 0b00010000 | x | | x | + * | expiry | 0b00100000 | x | | | + * | protected | 0b01000000 | | x | | * Note: (Parent) Owner/Spender means parent owner or current owner or current token spender. */ struct MutableRecord { // The resolver address. address resolver; - // Duration in second(s) to live. - uint64 ttl; - // The record owner. + // The record owner. This field must equal to the owner of token. address owner; // Expiry timestamp. uint64 expiry; @@ -68,15 +70,16 @@ interface INSUnified is IERC721Metadata { /// @dev Emitted when a base URI is updated. event BaseURIUpdated(address indexed operator, string newURI); + /// @dev Emitted when the grace period for all domain is updated. event GracePeriodUpdated(address indexed operator, uint64 newGracePeriod); /** - * @dev Emitted when records of node are updated. + * @dev Emitted when the record of node is updated. * @param indicator The binary index of updated fields. Eg, 0b10101011 means fields at position 1, 2, 4, 6, 8 (right * to left) needs to be updated. * @param record The updated fields. */ - event RecordsUpdated(uint256 indexed node, ModifyingIndicator indicator, Record record); + event RecordUpdated(uint256 indexed node, ModifyingIndicator indicator, Record record); /** * @dev Returns the controller role. @@ -96,6 +99,11 @@ interface INSUnified is IERC721Metadata { */ function RESERVATION_ROLE() external pure returns (bytes32); + /** + * @dev Returns the max expiry value. + */ + function MAX_EXPIRY() external pure returns (uint64); + /** * @dev Returns the name hash output of a domain. */ @@ -145,43 +153,47 @@ interface INSUnified is IERC721Metadata { * - The token must be available. * - The method caller must be (parent) owner or approved spender. See struct {MutableRecord}. * - * Emits an event {RecordsUpdated}. + * Emits an event {RecordUpdated}. * * @param parentId The parent node to mint or create subnode. - * @param label The domain label. Eg, label is dukethor for domain dukethor.ron. + * @param label The domain label. Eg, label is duke for domain duke.ron. * @param resolver The resolver address. - * @param ttl Duration in second(s) to live. * @param owner The token owner. * @param duration Duration in second(s) to expire. Leave 0 to set as parent. */ - function mint(uint256 parentId, string calldata label, address resolver, uint64 ttl, address owner, uint64 duration) + function mint(uint256 parentId, string calldata label, address resolver, address owner, uint64 duration) external returns (uint64 expiryTime, uint256 id); /** - * @dev Returns all records of a domain. + * @dev Returns all record of a domain. * Reverts if the token is non existent. */ - function getRecords(uint256 id) external view returns (Record memory records, string memory domain); + function getRecord(uint256 id) external view returns (Record memory record); + + /** + * @dev Returns the domain name of id. + */ + function getDomain(uint256 id) external view returns (string memory domain); /** - * @dev Returns whether the requester is able to modify the records based on the updated index. + * @dev Returns whether the requester is able to modify the record based on the updated index. * Note: This method strictly follows the permission of struct {MutableRecord}. */ - function canSetRecords(address requester, uint256 id, ModifyingIndicator indicator) + function canSetRecord(address requester, uint256 id, ModifyingIndicator indicator) external view returns (bool, bytes4 error); /** - * @dev Sets records of existing token. Update operation for {Record.mut}. + * @dev Sets record of existing token. Update operation for {Record.mut}. * * Requirements: * - The method caller must have role based on the corresponding `indicator`. See struct {MutableRecord}. * - * Emits an event {RecordsUpdated}. + * Emits an event {RecordUpdated}. */ - function setRecords(uint256 id, ModifyingIndicator indicator, MutableRecord calldata records) external; + function setRecord(uint256 id, ModifyingIndicator indicator, MutableRecord calldata record) external; /** * @dev Reclaims ownership. Update operation for {Record.mut.owner}. @@ -190,7 +202,7 @@ interface INSUnified is IERC721Metadata { * - The method caller should have controller role. * - The method caller should be (parent) owner or approved spender. See struct {MutableRecord}. * - * Emits an event {RecordsUpdated}. + * Emits an event {RecordUpdated}. */ function reclaim(uint256 id, address owner) external; @@ -200,7 +212,7 @@ interface INSUnified is IERC721Metadata { * Requirements: * - The method caller should have controller role. * - * Emits an event {RecordsUpdated}. + * Emits an event {RecordUpdated}. */ function renew(uint256 id, uint64 duration) external; @@ -210,7 +222,7 @@ interface INSUnified is IERC721Metadata { * Requirements: * - The method caller must have controller role. * - * Emits an event {RecordsUpdated}. + * Emits an event {RecordUpdated}. */ function setExpiry(uint256 id, uint64 expiry) external; @@ -220,7 +232,7 @@ interface INSUnified is IERC721Metadata { * Requirements: * - The method caller must have protected setter role. * - * Emits events {RecordsUpdated}. + * Emits events {RecordUpdated}. */ function bulkSetProtected(uint256[] calldata ids, bool protected) external; } diff --git a/src/interfaces/IReverseRegistrar.sol b/src/interfaces/IReverseRegistrar.sol index 07ff5232..f3fe2d1e 100644 --- a/src/interfaces/IReverseRegistrar.sol +++ b/src/interfaces/IReverseRegistrar.sol @@ -1,10 +1,101 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-LicINSe-Identifier: UNLICINSED pragma solidity ^0.8.0; -interface IReverseRegistrar { +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 node The INS node hash of the reverse record. + */ + function claim(address addr) external returns (bytes32 node); + + /** + * @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 node The INS node hash of the reverse record. + */ + function claimWithResolver(address addr, address resolver) external returns (bytes32 node); + + /** + * @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 (bytes32); +} + +interface IReverseRegistrar is IERC181, IERC165 { + /// @dev Error: The provided id is not child node of `ADDR_REVERSE_NODE` + error InvalidNode(); + /// @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, bytes32 indexed node); + /// @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 the address reverse role. + */ + function ADDR_REVERSE_NODE() 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 (bytes32 node); + /** * @dev Returns address that the reverse node resolves for. * Eg. node namehash('{addr}.addr.reverse') will always resolve for `addr`. */ function getAddress(bytes32 node) external view returns (address); + + /** + * @dev Returns the node hash for a given account's reverse records. + * @param addr The address to hash + * @return The INS node hash. + */ + function computeNode(address addr) external pure returns (bytes32); } diff --git a/src/libraries/LibModifyingField.sol b/src/libraries/LibModifyingField.sol index 4f4cfce8..47b2712e 100644 --- a/src/libraries/LibModifyingField.sol +++ b/src/libraries/LibModifyingField.sol @@ -8,7 +8,6 @@ enum ModifyingField { ParentId, Label, Resolver, - Ttl, Owner, Expiry, Protected diff --git a/src/libraries/LibStrAddrConvert.sol b/src/libraries/LibStrAddrConvert.sol new file mode 100644 index 00000000..e484323b --- /dev/null +++ b/src/libraries/LibStrAddrConvert.sol @@ -0,0 +1,60 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +library LibStrAddrConvert { + error InvalidStringLength(); + error InvalidCharacter(bytes1 char); + + /// @dev Lookup constant for method. See more detail at https://eips.ethereum.org/EIPS/eip-181 + + bytes32 private constant LOOKUP = 0x3031323334353637383961626364656600000000000000000000000000000000; + + /** + * @dev Converts an address to string. + */ + function toString(address addr) internal pure returns (string memory stringifiedAddr) { + assembly ("memory-safe") { + mstore(stringifiedAddr, 40) + let ptr := add(stringifiedAddr, 0x20) + for { let i := 40 } gt(i, 0) { } { + i := sub(i, 1) + mstore8(add(i, ptr), byte(and(addr, 0xf), LOOKUP)) + addr := div(addr, 0x10) + + i := sub(i, 1) + mstore8(add(i, ptr), byte(and(addr, 0xf), LOOKUP)) + addr := div(addr, 0x10) + } + } + } + + /** + * @dev Converts string to address. + * Reverts if the string length is not equal to 40. + */ + function parseAddr(string memory stringifiedAddr) internal pure returns (address) { + unchecked { + if (bytes(stringifiedAddr).length != 40) revert InvalidStringLength(); + uint160 addr; + for (uint256 i = 0; i < 40; i += 2) { + addr *= 0x100; + addr += uint160(_hexCharToDec(bytes(stringifiedAddr)[i])) * 0x10; + addr += _hexCharToDec(bytes(stringifiedAddr)[i + 1]); + } + return address(addr); + } + } + + /** + * @dev Converts a hex char (0-9, a-f, A-F) to decimal number. + * Reverts if the char is invalid. + */ + function _hexCharToDec(bytes1 c) private pure returns (uint8 r) { + unchecked { + if ((bytes1("a") <= c) && (c <= bytes1("f"))) r = uint8(c) - 87; + else if ((bytes1("A") <= c) && (c <= bytes1("F"))) r = uint8(c) - 55; + else if ((bytes1("0") <= c) && (c <= bytes1("9"))) r = uint8(c) - 48; + else revert InvalidCharacter(c); + } + } +} diff --git a/src/resolvers/PublicResolver.sol b/src/resolvers/PublicResolver.sol index 2b27da20..c79294b2 100644 --- a/src/resolvers/PublicResolver.sol +++ b/src/resolvers/PublicResolver.sol @@ -38,7 +38,6 @@ contract PublicResolver is /// @dev The RNS Unified contract INSUnified internal _rnsUnified; - /// @dev The reverse registrar contract IReverseRegistrar internal _reverseRegistrar; @@ -117,9 +116,8 @@ contract PublicResolver is } /// @inheritdoc IAddressResolver - function setAddr(bytes32 node, address addr_) external onlyAuthorized(node) { + function setAddr(bytes32 node, address /* addr_ */) external view onlyAuthorized(node) { revert("PublicResolver: Cannot set address"); - _setAddr(node, addr_); } /// @inheritdoc IContentHashResolver @@ -159,7 +157,7 @@ contract PublicResolver is /// @inheritdoc IPublicResolver function isAuthorized(bytes32 node, address account) public view returns (bool authorized) { - (authorized,) = _rnsUnified.canSetRecords(account, uint256(node), USER_FIELDS_INDICATOR); + (authorized,) = _rnsUnified.canSetRecord(account, uint256(node), USER_FIELDS_INDICATOR); } /// @dev Override {IAddressResolvable-addr}. diff --git a/src/types/ModifyingIndicator.sol b/src/types/ModifyingIndicator.sol index b194ab89..2c03ccf2 100644 --- a/src/types/ModifyingIndicator.sol +++ b/src/types/ModifyingIndicator.sol @@ -5,20 +5,40 @@ type ModifyingIndicator is uint256; using { hasAny } for ModifyingIndicator global; using { or as | } for ModifyingIndicator global; +using { and as & } for ModifyingIndicator global; +using { eq as == } for ModifyingIndicator global; +using { not as ~ } for ModifyingIndicator global; +using { neq as != } for ModifyingIndicator global; /// @dev Indicator for modifying immutable fields: Depth, ParentId, Label. See struct {INSUnified.ImmutableRecord}. ModifyingIndicator constant IMMUTABLE_FIELDS_INDICATOR = ModifyingIndicator.wrap(0x7); -/// @dev Indicator for modifying user fields: Resolver, Ttl, Owner. See struct {INSUnified.ImmutableRecord}. -ModifyingIndicator constant USER_FIELDS_INDICATOR = ModifyingIndicator.wrap(0x38); +/// @dev Indicator for modifying user fields: Resolver, Owner. See struct {INSUnified.MutableRecord}. +ModifyingIndicator constant USER_FIELDS_INDICATOR = ModifyingIndicator.wrap(0x18); /// @dev Indicator when modifying all of the fields in {ModifyingField}. ModifyingIndicator constant ALL_FIELDS_INDICATOR = ModifyingIndicator.wrap(type(uint256).max); +function eq(ModifyingIndicator self, ModifyingIndicator other) pure returns (bool) { + return ModifyingIndicator.unwrap(self) == ModifyingIndicator.unwrap(other); +} + +function neq(ModifyingIndicator self, ModifyingIndicator other) pure returns (bool) { + return !eq(self, other); +} + +function not(ModifyingIndicator self) pure returns (ModifyingIndicator) { + return ModifyingIndicator.wrap(~ModifyingIndicator.unwrap(self)); +} + function or(ModifyingIndicator self, ModifyingIndicator other) pure returns (ModifyingIndicator) { return ModifyingIndicator.wrap(ModifyingIndicator.unwrap(self) | ModifyingIndicator.unwrap(other)); } +function and(ModifyingIndicator self, ModifyingIndicator other) pure returns (ModifyingIndicator) { + return ModifyingIndicator.wrap(ModifyingIndicator.unwrap(self) & ModifyingIndicator.unwrap(other)); +} + function hasAny(ModifyingIndicator self, ModifyingIndicator other) pure returns (bool) { - return ModifyingIndicator.unwrap(or(self, other)) != 0; + return self & other != ModifyingIndicator.wrap(0); } diff --git a/test/RNSUnified/RNSUnified.ERC721.t.sol b/test/RNSUnified/RNSUnified.ERC721.t.sol new file mode 100644 index 00000000..85388cab --- /dev/null +++ b/test/RNSUnified/RNSUnified.ERC721.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "./RNSUnified.t.sol"; + +contract RNSUnified_ERC721_Test is RNSUnifiedTest { + function test_TokenMetadata() external { + assertEq(_rns.name(), "Ronin Name Service"); + assertEq(_rns.symbol(), "RNS"); + } + + function testFuzz_WhenExpired_RevokeOwnership_ownerOf(MintParam calldata mintParam) external mintAs(_controller) { + (uint64 expiry, uint256 id) = _mint(_ronId, mintParam, _noError); + _warpToExpire(expiry); + assertEq(_rns.ownerOf(id), address(0x00)); + } + + function testFuzz_WhenExpired_RevokeApproval_getApproved(address approved, MintParam calldata mintParam) + external + validAccount(approved) + mintAs(_controller) + { + vm.assume(approved != mintParam.owner && approved != _admin); + (uint64 expiry, uint256 id) = _mint(_ronId, mintParam, _noError); + vm.prank(mintParam.owner); + _rns.setApprovalForAll(approved, true); + _warpToExpire(expiry); + address actualApproved = _rns.getApproved(id); + assertEq(actualApproved, address(0x00)); + } + + function testFuzz_UpdateRecordOwner_transferFrom(address newOwner, MintParam calldata mintParam) + external + validAccount(newOwner) + mintAs(_controller) + { + vm.assume(newOwner != _admin && newOwner != mintParam.owner); + (, uint256 id) = _mint(_ronId, mintParam, _noError); + vm.prank(mintParam.owner); + _rns.transferFrom(mintParam.owner, newOwner, id); + INSUnified.Record memory record = _rns.getRecord(id); + assertEq(record.mut.owner, newOwner); + } + + function testFuzz_WhenTransfered_LostProtected(address newOwner, MintParam calldata mintParam) + external + validAccount(newOwner) + mintAs(_controller) + { + vm.assume(newOwner != mintParam.owner); + (, uint256 id) = _mint(_ronId, mintParam, _noError); + + uint256[] memory ids = new uint256[](1); + ids[0] = id; + vm.prank(_protectedSettler); + _rns.bulkSetProtected(ids, true); + + INSUnified.Record memory record = _rns.getRecord(id); + assertTrue(record.mut.protected); + + vm.prank(mintParam.owner); + _rns.transferFrom(mintParam.owner, newOwner, id); + + record = _rns.getRecord(id); + assertFalse(record.mut.protected); + } +} diff --git a/test/RNSUnified/RNSUnified.bulkSetProtected.t.sol b/test/RNSUnified/RNSUnified.bulkSetProtected.t.sol new file mode 100644 index 00000000..0e3c7275 --- /dev/null +++ b/test/RNSUnified/RNSUnified.bulkSetProtected.t.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "./RNSUnified.t.sol"; + +contract RNSUnified_BulkSetProtected_Test is RNSUnifiedTest { + using LibModifyingField for ModifyingField; + + uint256 public constant MAX_FUZZ_INPUT = 100; + + modifier boundFuzzArrLength(uint256 length) { + vm.assume(length <= MAX_FUZZ_INPUT); + _; + } + + function testFuzz_RevertWhenNotMinted_bulkSetProtected(bool protected, MintParam calldata mintParam) external { + uint256 id = _toId(_ronId, mintParam.name); + uint256[] memory ids = new uint256[](1); + ids[0] = id; + vm.expectRevert(INSUnified.Unexists.selector); + vm.prank(_protectedSettler); + _rns.bulkSetProtected(ids, protected); + } + + function testGas_WhenMinted_AsProtectedSettler_bulkSetProtected(MintParam[] calldata mintParams) + external + mintAs(_controller) + boundFuzzArrLength(mintParams.length) + { + uint256[] memory ids = _mintBulk(mintParams); + + vm.prank(_protectedSettler); + _rns.bulkSetProtected(ids, true); + + vm.pauseGasMetering(); + for (uint256 i; i < ids.length;) { + assertTrue(_rns.getRecord(ids[i]).mut.protected); + + unchecked { + ++i; + } + } + vm.resumeGasMetering(); + } + + function testGas_WhenMinted_AsProtectedSettler_bulkSetUnprotected(MintParam[] calldata mintParams) + external + mintAs(_controller) + boundFuzzArrLength(mintParams.length) + { + uint256[] memory ids = _mintBulk(mintParams); + + vm.pauseGasMetering(); + vm.prank(_protectedSettler); + _rns.bulkSetProtected(ids, true); + + vm.resumeGasMetering(); + vm.prank(_protectedSettler); + _rns.bulkSetProtected(ids, false); + vm.pauseGasMetering(); + + for (uint256 i; i < ids.length;) { + assertFalse(_rns.getRecord(ids[i]).mut.protected); + + unchecked { + ++i; + } + } + vm.resumeGasMetering(); + } +} diff --git a/test/RNSUnified/RNSUnified.mint.t.sol b/test/RNSUnified/RNSUnified.mint.t.sol new file mode 100644 index 00000000..331108c6 --- /dev/null +++ b/test/RNSUnified/RNSUnified.mint.t.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "./RNSUnified.t.sol"; + +contract RNSUnified_Mint_Test is RNSUnifiedTest { + function testGas_AsController_mint(MintParam calldata mintParam) external mintAs(_controller) { + _mint(_ronId, mintParam, _noError); + } + + function testGas_AsAdmin_mint(MintParam calldata mintParam) external mintAs(_admin) { + _mint(_ronId, mintParam, _noError); + } + + function testFuzz_AsParentApproved_mint(address approved, MintParam calldata mintParam) + external + validAccount(approved) + mintAs(approved) + { + vm.assume(_admin != approved); + vm.prank(_admin); + _rns.approve(approved, _ronId); + _mint(_ronId, mintParam, _noError); + } + + function testFuzz_RevertWhenPaused_AsController_mint(MintParam calldata mintParam) external mintAs(_controller) { + vm.prank(_pauser); + _rns.pause(); + _mint(_ronId, mintParam, Error(true, "Pausable: paused")); + } + + function testFuzz_RevertWhenRonIdTransfered_AsController_mint(address newAdmin, MintParam calldata mintParam) + external + mintAs(_controller) + validAccount(newAdmin) + { + vm.assume(_admin != newAdmin && newAdmin != _controller); + vm.prank(_admin); + _rns.safeTransferFrom(_admin, newAdmin, _ronId); + _mint(_ronId, mintParam, Error(true, abi.encodeWithSelector(INSUnified.Unauthorized.selector))); + } + + function testFuzz_RevertIfUnauthorized_mint(address any, MintParam calldata mintParam) external mintAs(any) { + vm.assume(any != _admin && any != _controller); + _mint(_ronId, mintParam, Error(true, abi.encodeWithSelector(INSUnified.Unauthorized.selector))); + } + + function testFuzz_AsController_RevertWhenNotExpired_Remint(address otherOwner, MintParam memory mintParam) + external + mintAs(_controller) + { + _mint(_ronId, mintParam, _noError); + mintParam.owner = otherOwner; + _mint(_ronId, mintParam, Error(true, abi.encodeWithSelector(INSUnified.Unavailable.selector))); + } + + function testFuzz_AsController_WhenExpired_Remint(MintParam calldata mintParam) external mintAs(_controller) { + vm.assume(block.timestamp + mintParam.duration < _ronExpiry); + (uint64 expiry, uint256 id) = _mint(_ronId, mintParam, _noError); + assertFalse(_rns.available(id)); + _warpToExpire(expiry); + assertTrue(_rns.available(id)); + _mint(_ronId, mintParam, _noError); + } + + function testFuzz_AsController_RevertWhenControllerUnapproved_mint(MintParam calldata mintParam) + external + mintAs(_controller) + { + vm.prank(_admin); + _rns.setApprovalForAll(_controller, false); + _mint(_ronId, mintParam, Error(true, abi.encodeWithSelector(INSUnified.Unauthorized.selector))); + } + + function testFuzz_WhenRemint_LostProtected(MintParam calldata mintParam) external mintAs(_controller) { + (uint64 expiry, uint256 id) = _mint(_ronId, mintParam, _noError); + uint256[] memory ids = new uint256[](1); + ids[0] = id; + + vm.prank(_protectedSettler); + _rns.bulkSetProtected(ids, true); + + _warpToExpire(expiry); + _mint(_ronId, mintParam, _noError); + + assertFalse(_rns.getRecord(id).mut.protected); + } +} diff --git a/test/RNSUnified/RNSUnified.reclaim.t.sol b/test/RNSUnified/RNSUnified.reclaim.t.sol new file mode 100644 index 00000000..84a0480b --- /dev/null +++ b/test/RNSUnified/RNSUnified.reclaim.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "./RNSUnified.t.sol"; + +contract RNSUnified_Reclaim_Test is RNSUnifiedTest { + using LibModifyingField for *; + + function test_WhenMintedAndTransferedToNewOwner_AsController_ReclaimOwnership_reclaim( + address newOwner, + MintParam calldata mintParam + ) external mintAs(_controller) reclaimAs(_controller) validAccount(newOwner) { + vm.assume(newOwner != mintParam.owner); + (, uint256 id) = _mint(_ronId, mintParam, _noError); + + vm.prank(mintParam.owner); + _rns.transferFrom(mintParam.owner, newOwner, id); + + assertEq(newOwner, _rns.ownerOf(id)); + assertEq(newOwner, _rns.getRecord(id).mut.owner); + + _reclaim(id, mintParam.owner); + } + + function test_WhenMinted_AsParentOwner_ReclaimOwnership_reclaim(address newOwner, MintParam calldata mintParam) + external + mintAs(_controller) + reclaimAs(_admin) + validAccount(newOwner) + { + vm.assume(newOwner != mintParam.owner); + (, uint256 id) = _mint(_ronId, mintParam, _noError); + + _reclaim(id, newOwner); + } + + function test_WhenMinted_AsApproved_ReclaimOwnership_reclaim( + address approved, + address newOwner, + MintParam calldata mintParam + ) external mintAs(_controller) reclaimAs(approved) validAccount(approved) validAccount(newOwner) { + vm.assume(approved != mintParam.owner); + vm.assume(newOwner != mintParam.owner); + + (, uint256 id) = _mint(_ronId, mintParam, _noError); + vm.prank(mintParam.owner); + _rns.approve(approved, id); + + _reclaim(id, newOwner); + } +} diff --git a/test/RNSUnified/RNSUnified.setExpiry.t.sol b/test/RNSUnified/RNSUnified.setExpiry.t.sol new file mode 100644 index 00000000..3d7a64f2 --- /dev/null +++ b/test/RNSUnified/RNSUnified.setExpiry.t.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "./RNSUnified.t.sol"; + +contract RNSUnified_SetExpiry_Test is RNSUnifiedTest { + using Strings for *; + + function testGas_AsController_Renew(MintParam calldata mintParam, uint64 renewDuration) external mintAs(_controller) { + vm.assume(renewDuration > mintParam.duration); + (, uint256 id) = _mint(_ronId, mintParam, _noError); + + vm.prank(_controller); + _rns.renew(id, renewDuration); + } + + function testGas_AsController_SetExpiry(MintParam calldata mintParam, uint64 renewExpiry) + external + mintAs(_controller) + { + vm.assume(renewExpiry > block.timestamp + mintParam.duration); + (, uint256 id) = _mint(_ronId, mintParam, _noError); + + vm.prank(_controller); + _rns.setExpiry(id, renewExpiry); + } + + function testFuzz_RevertWhenAvailable_Renew(MintParam calldata mintParam, uint64 renewDuration) + external + mintAs(_controller) + { + uint256 id = _toId(_ronId, mintParam.name); + vm.prank(_controller); + vm.expectRevert(INSUnified.NameMustBeRegisteredOrInGracePeriod.selector); + _rns.renew(id, renewDuration); + } + + function testFuzz_RevertWhenAvailable_SetExpiry(MintParam calldata mintParam, uint64 renewExpiry) + external + mintAs(_controller) + { + uint256 id = _toId(_ronId, mintParam.name); + vm.prank(_controller); + vm.expectRevert(INSUnified.NameMustBeRegisteredOrInGracePeriod.selector); + _rns.renew(id, renewExpiry); + } + + function testFuzz_RevertIfNewExpiryLessThanCurrentExpiry_SetExpiry(MintParam calldata mintParam, uint64 renewExpiry) + external + mintAs(_controller) + { + (uint64 expiry, uint256 id) = _mint(_ronId, mintParam, _noError); + vm.assume(renewExpiry < expiry); + + vm.prank(_controller); + vm.expectRevert(INSUnified.ExpiryTimeMustBeLargerThanTheOldOne.selector); + _rns.setExpiry(id, renewExpiry); + } + + function testFuzz_RevertIf_AsUnauthorized_Renew(address any, MintParam calldata mintParam, uint64 renewDuration) + external + mintAs(_controller) + { + vm.assume(any != _controller && any != _admin); + vm.assume(renewDuration > mintParam.duration); + (, uint256 id) = _mint(_ronId, mintParam, _noError); + + bytes memory revertMessage = bytes( + string.concat( + "AccessControl: account ", any.toHexString(), " is missing role ", uint256(_rns.CONTROLLER_ROLE()).toHexString() + ) + ); + vm.prank(any); + vm.expectRevert(revertMessage); + _rns.renew(id, renewDuration); + } + + function testFuzz_RevertIf_AsUnauthorized_SetExpiry(address any, MintParam calldata mintParam, uint64 renewExpiry) + external + validAccount(any) + mintAs(_controller) + { + vm.assume(renewExpiry > block.timestamp + mintParam.duration); + vm.assume(any != _controller && any != _admin); + (, uint256 id) = _mint(_ronId, mintParam, _noError); + + bytes memory revertMessage = bytes( + string.concat( + "AccessControl: account ", any.toHexString(), " is missing role ", uint256(_rns.CONTROLLER_ROLE()).toHexString() + ) + ); + vm.prank(any); + vm.expectRevert(revertMessage); + _rns.setExpiry(id, renewExpiry); + } +} diff --git a/test/RNSUnified/RNSUnified.setRecord.t.sol b/test/RNSUnified/RNSUnified.setRecord.t.sol new file mode 100644 index 00000000..55396b36 --- /dev/null +++ b/test/RNSUnified/RNSUnified.setRecord.t.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "./RNSUnified.t.sol"; + +contract RNSUnified_SetRecord_Test is RNSUnifiedTest { + using LibModifyingField for ModifyingField; + + INSUnified.MutableRecord internal _emptyMutRecord; + + function testFuzz_WhenMinted_AsProtectedSettler_CanSetProtectedField_canSetRecord( + MintParam calldata mintParam, + INSUnified.MutableRecord calldata mutRecord + ) external mintAs(_controller) setRecordAs(_protectedSettler) { + (, uint256 id) = _mint(_ronId, mintParam, _noError); + (bool allowed, bytes4 error) = _rns.canSetRecord(_protectedSettler, id, ModifyingField.Protected.indicator()); + assertTrue(allowed, _errorIndentifier[error]); + + _setRecord(id, ModifyingField.Protected.indicator(), mutRecord, _noError); + } + + function testFuzz_WhenMinted_AsController_CanSetMutableField_canSetRecord( + ModifyingIndicator indicator, + MintParam calldata mintParam, + INSUnified.MutableRecord calldata mutRecord + ) external mintAs(_controller) setRecordAs(_controller) { + vm.assume(!indicator.hasAny(IMMUTABLE_FIELDS_INDICATOR)); + vm.assume(!indicator.hasAny(ModifyingField.Protected.indicator())); + if (indicator.hasAny(ModifyingField.Owner.indicator())) { + _assumeValidAccount(mutRecord.owner); + } + if (indicator.hasAny(ModifyingField.Expiry.indicator())) { + vm.assume(mutRecord.expiry > block.timestamp + mintParam.duration); + } + (, uint256 id) = _mint(_ronId, mintParam, _noError); + (bool allowed, bytes4 error) = _rns.canSetRecord(_controller, id, indicator); + assertTrue(allowed, _errorIndentifier[error]); + + _setRecord(id, indicator, mutRecord, _noError); + } + + function testFuzz_WhenMinted_AsController_CannotSetProtectedField_canSetRecord( + ModifyingIndicator indicator, + MintParam calldata mintParam + ) external mintAs(_controller) setRecordAs(_controller) { + vm.assume(indicator.hasAny(ModifyingField.Protected.indicator())); + (, uint256 id) = _mint(_ronId, mintParam, _noError); + (bool allowed, bytes4 error) = _rns.canSetRecord(_controller, id, indicator); + assertFalse(allowed, _errorIndentifier[error]); + + _setRecord(id, indicator, _emptyMutRecord, Error(true, abi.encodeWithSelector(error))); + } + + function testFuzz_WhenMinted_AsProtectedSettler_CannotSetImmutableField_canSetRecord( + ModifyingIndicator indicator, + MintParam calldata mintParam + ) external mintAs(_controller) setRecordAs(_protectedSettler) { + vm.assume(indicator != ModifyingIndicator.wrap(0x00)); + vm.assume(indicator.hasAny(IMMUTABLE_FIELDS_INDICATOR)); + + (, uint256 id) = _mint(_ronId, mintParam, _noError); + (bool allowed, bytes4 error) = _rns.canSetRecord(_protectedSettler, id, indicator); + assertFalse(allowed, _errorIndentifier[error]); + + _setRecord(id, indicator, _emptyMutRecord, Error(true, abi.encodeWithSelector(error))); + } + + function testFuzz_WhenMinted_AsProtectedSettler_CannotSetOtherMutableField_canSetRecord( + ModifyingIndicator indicator, + MintParam calldata mintParam + ) external mintAs(_controller) setRecordAs(_protectedSettler) { + vm.assume(indicator != ModifyingIndicator.wrap(0x00)); + vm.assume(indicator.hasAny(USER_FIELDS_INDICATOR)); + vm.assume(indicator.hasAny(IMMUTABLE_FIELDS_INDICATOR)); + + (, uint256 id) = _mint(_ronId, mintParam, _noError); + (bool allowed, bytes4 error) = _rns.canSetRecord(_protectedSettler, id, indicator); + assertFalse(allowed, _errorIndentifier[error]); + + _setRecord(id, indicator, _emptyMutRecord, Error(true, abi.encodeWithSelector(error))); + } + + function testFuzz_WhenNotMinted_AsProtectedSettler_CannotSetProtectedField_canSetRecord( + ModifyingIndicator indicator, + MintParam calldata mintParam + ) external setRecordAs(_protectedSettler) { + vm.assume(!indicator.hasAny(IMMUTABLE_FIELDS_INDICATOR)); + vm.assume(indicator.hasAny(ModifyingField.Protected.indicator())); + uint256 id = _toId(_ronId, mintParam.name); + (bool allowed, bytes4 error) = _rns.canSetRecord(_protectedSettler, id, indicator); + assertFalse(allowed, _errorIndentifier[error]); + assertEq(error, INSUnified.Unexists.selector, _errorIndentifier[error]); + + _setRecord(id, indicator, _emptyMutRecord, Error(true, abi.encodeWithSelector(error))); + } + + function testFuzz_WhenNotMinted_AsProtectedSettler_CannotSetImmutableField_canSetRecord( + ModifyingIndicator indicator, + MintParam calldata mintParam + ) external setRecordAs(_protectedSettler) { + vm.assume(indicator != ModifyingIndicator.wrap(0x00)); + vm.assume(indicator.hasAny(IMMUTABLE_FIELDS_INDICATOR)); + + uint256 id = _toId(_ronId, mintParam.name); + (bool allowed, bytes4 error) = _rns.canSetRecord(_protectedSettler, id, indicator); + assertFalse(allowed, _errorIndentifier[error]); + assertEq(error, INSUnified.CannotSetImmutableField.selector, _errorIndentifier[error]); + + _setRecord(id, indicator, _emptyMutRecord, Error(true, abi.encodeWithSelector(error))); + } + + function testFuzz_WhenNotMinted_AsProtectedSettler_CannotSetOtherMutableField_canSetRecord( + ModifyingIndicator indicator, + MintParam calldata mintParam + ) external setRecordAs(_protectedSettler) { + vm.assume(indicator != ModifyingIndicator.wrap(0x00)); + vm.assume(indicator.hasAny(USER_FIELDS_INDICATOR)); + vm.assume(!indicator.hasAny(IMMUTABLE_FIELDS_INDICATOR)); + + uint256 id = _toId(_ronId, mintParam.name); + (bool allowed, bytes4 error) = _rns.canSetRecord(_protectedSettler, id, indicator); + assertFalse(allowed, _errorIndentifier[error]); + assertEq(error, INSUnified.Unexists.selector, _errorIndentifier[error]); + + _setRecord(id, indicator, _emptyMutRecord, Error(true, abi.encodeWithSelector(error))); + } +} diff --git a/test/RNSUnified/RNSUnified.t.sol b/test/RNSUnified/RNSUnified.t.sol new file mode 100644 index 00000000..4d6ca3c4 --- /dev/null +++ b/test/RNSUnified/RNSUnified.t.sol @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { console2, Test } from "forge-std/Test.sol"; +import "@rns-contracts/RNSUnified.sol"; + +abstract contract RNSUnifiedTest is Test { + using Strings for *; + using LibModifyingField for *; + + /// @dev Emitted when a base URI is updated. + event BaseURIUpdated(address indexed operator, string newURI); + /// @dev Emitted when the grace period for all domain is updated. + event GracePeriodUpdated(address indexed operator, uint64 newGracePeriod); + + /** + * @dev Emitted when the record of node is updated. + * @param indicator The binary index of updated fields. Eg, 0b10101011 means fields at position 1, 2, 4, 6, 8 (right + * to left) needs to be updated. + * @param record The updated fields. + */ + event RecordUpdated(uint256 indexed node, ModifyingIndicator indicator, INSUnified.Record record); + + struct MintParam { + address owner; + string name; + address resolver; + uint64 duration; + } + + struct Error { + bool shouldThrow; + bytes revertMessage; + } + + uint64 public constant GRACE_PERIOD = 30 days; + string public constant BASE_URI = "https://example.com/"; + + address internal _admin; + address internal _pauser; + address internal _proxyAdmin; + address internal _controller; + address internal _protectedSettler; + + uint256 internal _ronId; + Error internal _noError; + RNSUnified internal _rns; + uint64 internal _ronExpiry; + + /// @dev state changes variables + address internal $minter; + address internal $reclaimer; + address internal $recordSetter; + bool internal $mintGasOff; + + mapping(string name => bool used) internal _usedName; + mapping(bytes4 errorCode => string indentifier) internal _errorIndentifier; + + modifier validAccount(address addr) { + _assumeValidAccount(addr); + _; + } + + modifier mintAs(address addr) { + _assumeValidAccount(addr); + $minter = addr; + _; + } + + modifier reclaimAs(address addr) { + _assumeValidAccount(addr); + $reclaimer = addr; + _; + } + + modifier setRecordAs(address addr) { + _assumeValidAccount(addr); + $recordSetter = addr; + _; + } + + modifier mintGasOff() { + $mintGasOff = true; + _; + } + + function setUp() external { + _admin = makeAddr("admin"); + _pauser = makeAddr("pauser"); + _controller = makeAddr("controller"); + _proxyAdmin = makeAddr("proxyAdmin"); + _protectedSettler = makeAddr("protectedSettler"); + + address logic = address(new RNSUnified()); + _rns = RNSUnified( + address( + new TransparentUpgradeableProxy(logic, _proxyAdmin, abi.encodeCall(RNSUnified.initialize, (_admin, _pauser, _controller, _protectedSettler, GRACE_PERIOD, BASE_URI))) + ) + ); + + vm.label(logic, "RNSUnfied::Logic"); + vm.label(address(_rns), "RNSUnfied::Proxy"); + + _errorIndentifier[INSUnified.Unexists.selector] = "Unexists"; + _errorIndentifier[INSUnified.Unauthorized.selector] = "Unauthorized"; + _errorIndentifier[INSUnified.MissingControllerRole.selector] = "MissingControllerRole"; + _errorIndentifier[INSUnified.CannotSetImmutableField.selector] = "CannotSetImmutableField"; + _errorIndentifier[INSUnified.MissingProtectedSettlerRole.selector] = "MissingProtectedSettlerRole"; + + vm.warp(block.timestamp + GRACE_PERIOD + 1 seconds); + vm.startPrank(_admin); + (_ronExpiry, _ronId) = _rns.mint(0x0, "ron", address(0), _admin, _rns.MAX_EXPIRY()); + _rns.setApprovalForAll(_controller, true); + vm.stopPrank(); + } + + function _assumeValidAccount(address addr) internal { + vm.assume(addr != _proxyAdmin); + assumeAddressIsNot( + addr, AddressType.NonPayable, AddressType.ForgeAddress, AddressType.ZeroAddress, AddressType.Precompile + ); + } + + function _mint(uint256 parentId, MintParam memory mintParam, Error memory error) + internal + noGasMetering + validAccount(mintParam.owner) + returns (uint64 expiry, uint256 id) + { + require($minter != address(0), "Minter for RNSUnified::mint not set!"); + vm.assume(block.timestamp + mintParam.duration < _ronExpiry); + if (error.shouldThrow) vm.expectRevert(error.revertMessage); + + if (!$mintGasOff) vm.resumeGasMetering(); + vm.prank($minter); + (expiry, id) = _rns.mint(parentId, mintParam.name, mintParam.resolver, mintParam.owner, mintParam.duration); + if (!$mintGasOff) vm.pauseGasMetering(); + + if (!error.shouldThrow) _assertMint(parentId, id, mintParam); + } + + function _mintBulk(MintParam[] calldata mintParams) internal mintGasOff noGasMetering returns (uint256[] memory ids) { + uint256 ronId = _ronId; + MintParam memory mintParam; + Error memory noError = _noError; + uint256 length = mintParams.length; + ids = new uint256[](length); + + for (uint256 i; i < length;) { + mintParam = mintParams[i]; + vm.assume(!_usedName[mintParam.name]); + (, ids[i]) = _mint(ronId, mintParam, noError); + _usedName[mintParam.name] = true; + + unchecked { + ++i; + } + } + } + + function _reclaim(uint256 id, address owner) internal { + require($reclaimer != address(0), "Reclaimer for RNSUnified::reclaim not set!"); + INSUnified.Record memory emittedRecord; + emittedRecord.mut.owner = owner; + + vm.expectEmit(address(_rns)); + emit RecordUpdated(id, ModifyingField.Owner.indicator(), emittedRecord); + vm.prank($reclaimer); + _rns.reclaim(id, owner); + + assertEq(owner, _rns.ownerOf(id)); + assertEq(owner, _rns.getRecord(id).mut.owner); + } + + function _setRecord( + uint256 id, + ModifyingIndicator indicator, + INSUnified.MutableRecord memory mutRecord, + Error memory error + ) internal { + require($recordSetter != address(0), "Record Setter for RNSUnified::setRecord not set!"); + + INSUnified.MutableRecord memory mutRecordBefore; + INSUnified.MutableRecord memory mutRecordAfter; + INSUnified.Record memory filledRecord; + + if (error.shouldThrow) { + vm.expectRevert(error.revertMessage); + } else { + mutRecordBefore = _rns.getRecord(id).mut; + filledRecord = _fillMutRecord(indicator, mutRecord); + // vm.expectEmit(address(_rns)); + // emit RecordUpdated(id, indicator, filledRecord); + } + vm.prank($recordSetter); + _rns.setRecord(id, indicator, mutRecord); + + if (!error.shouldThrow) { + mutRecordAfter = _rns.getRecord(id).mut; + _assertRecord(id, indicator, filledRecord.mut, mutRecordBefore, mutRecordAfter); + } + } + + function _warpToExpire(uint64 expiry) internal { + vm.warp(block.timestamp + expiry + 1 seconds); + } + + function _toId(uint256 parentId, string memory label) internal pure returns (uint256 id) { + bytes32 labelHash = keccak256(bytes(label)); + id = uint256(keccak256(abi.encode(parentId, labelHash))); + } + + function _fillMutRecord(ModifyingIndicator indicator, INSUnified.MutableRecord memory mutRecord) + internal + pure + returns (INSUnified.Record memory filledRecord) + { + if (indicator.hasAny(ModifyingField.Owner.indicator())) { + filledRecord.mut.owner = mutRecord.owner; + } + if (indicator.hasAny(ModifyingField.Resolver.indicator())) { + filledRecord.mut.resolver = mutRecord.resolver; + } + if (indicator.hasAny(ModifyingField.Expiry.indicator())) { + filledRecord.mut.expiry = mutRecord.expiry; + } + if (indicator.hasAny(ModifyingField.Protected.indicator())) { + filledRecord.mut.protected = mutRecord.protected; + } + } + + function _assertRecord( + uint256 id, + ModifyingIndicator indicator, + INSUnified.MutableRecord memory filledMut, + INSUnified.MutableRecord memory mutRecordBefore, + INSUnified.MutableRecord memory mutRecordAfter + ) internal { + if (indicator.hasAny(ModifyingField.Owner.indicator())) { + assertEq(mutRecordAfter.owner, filledMut.owner); + if (mutRecordAfter.expiry >= block.timestamp) { + assertEq(_rns.ownerOf(id), filledMut.owner); + } + } else { + assertEq(mutRecordBefore.owner, mutRecordAfter.owner); + if (mutRecordAfter.expiry >= block.timestamp) { + assertEq(_rns.ownerOf(id), mutRecordBefore.owner); + } + } + if (indicator.hasAny(ModifyingField.Protected.indicator())) { + assertEq(mutRecordAfter.protected, filledMut.protected); + } else { + assertEq(mutRecordAfter.protected, mutRecordBefore.protected); + } + if (indicator.hasAny(ModifyingField.Expiry.indicator())) { + if (mutRecordAfter.expiry >= block.timestamp) { + if (!_rns.hasRole(_rns.RESERVATION_ROLE(), _rns.ownerOf(id))) { + assertEq(mutRecordAfter.expiry, filledMut.expiry); + } else { + assertEq(mutRecordAfter.expiry, _rns.MAX_EXPIRY()); + } + } + } else { + assertEq(mutRecordAfter.expiry, mutRecordBefore.expiry); + } + if (indicator.hasAny(ModifyingField.Resolver.indicator())) { + assertEq(mutRecordAfter.resolver, filledMut.resolver); + } else { + assertEq(mutRecordAfter.resolver, mutRecordBefore.resolver); + } + } + + function _assertMint(uint256 parentId, uint256 id, MintParam memory mintParam) internal { + string memory domain = _rns.getDomain(id); + string memory parentDomain = _rns.getDomain(parentId); + INSUnified.Record memory record = _rns.getRecord(id); + INSUnified.Record memory parentRecord = _rns.getRecord(parentId); + + string memory name = mintParam.name; + assertEq(_rns.ownerOf(id), mintParam.owner); + assertEq(record.immut.label, name); + assertEq(record.mut.protected, false); + assertEq(record.mut.resolver, mintParam.resolver); + assertEq(record.immut.depth, parentRecord.immut.depth + 1); + assertEq(domain, string.concat(name, ".", parentDomain)); + assertEq(domain, string.concat(name, ".", parentRecord.immut.label)); + assertEq(_rns.tokenURI(id), string.concat(BASE_URI, address(_rns).toHexString(), "/", id.toString())); + } +} diff --git a/test/libraries/LibStrAddrConvert.t.sol b/test/libraries/LibStrAddrConvert.t.sol new file mode 100644 index 00000000..4627b878 --- /dev/null +++ b/test/libraries/LibStrAddrConvert.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "src/libraries/LibStrAddrConvert.sol"; + +contract LibStrAddrConvertTest is Test { + function test_AddressToString(address addr) public { + string memory expected = withoutHexPrefix(Strings.toHexString(addr)); + string memory actual = LibStrAddrConvert.toString(addr); + assertEq(expected, actual); + } + + function test_StringToAddress(address expected) public { + string memory stringifiedAddr = withoutHexPrefix(Strings.toHexString(expected)); + address actual = LibStrAddrConvert.parseAddr(stringifiedAddr); + assertEq(expected, actual); + } + + function withoutHexPrefix(string memory str) public pure returns (string memory) { + if (bytes(str)[0] == bytes1("0") && bytes(str)[1] == bytes1("x")) { + uint256 length = bytes(str).length; + bytes memory out = new bytes(length - 2); + for (uint256 i = 2; i < length; i++) { + out[i - 2] = bytes(str)[i]; + } + return string(out); + } + return str; + } +}