From c307e7842b33b7fee1644b3f44f7c361d0a12346 Mon Sep 17 00:00:00 2001 From: katzman Date: Tue, 1 Oct 2024 13:54:56 -0700 Subject: [PATCH 1/6] Add upgradeable RegistrarController, make it EIP-7201 compliant' --- src/L2/UpgradeableRegistrarController.sol | 637 ++++++++++++++++++++++ 1 file changed, 637 insertions(+) create mode 100644 src/L2/UpgradeableRegistrarController.sol diff --git a/src/L2/UpgradeableRegistrarController.sol b/src/L2/UpgradeableRegistrarController.sol new file mode 100644 index 0000000..83c86f1 --- /dev/null +++ b/src/L2/UpgradeableRegistrarController.sol @@ -0,0 +1,637 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {EnumerableSetLib} from "solady/utils/EnumerableSetLib.sol"; +import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {OwnableUpgradeable} from "openzeppelin/access/OwnableUpgradeable.sol"; +import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {StringUtils} from "ens-contracts/ethregistrar/StringUtils.sol"; + +import {BASE_ETH_NODE, GRACE_PERIOD} from "src/util/Constants.sol"; +import {BaseRegistrar} from "./BaseRegistrar.sol"; +import {IDiscountValidator} from "./interface/IDiscountValidator.sol"; +import {IPriceOracle} from "./interface/IPriceOracle.sol"; +import {L2Resolver} from "./L2Resolver.sol"; +import {IReverseRegistrar} from "./interface/IReverseRegistrar.sol"; + +/// @title Registrar Controller +/// +/// @notice A permissioned controller for managing registering and renewing names against the `base` registrar. +/// This contract enables a `discountedRegister` flow which is validated by calling external implementations +/// of the `IDiscountValidator` interface. Pricing, denominated in wei, is determined by calling out to a +/// contract that implements `IPriceOracle`. +/// +/// Inspired by the ENS ETHRegistrarController: +/// https://github.com/ensdomains/ens-contracts/blob/staging/contracts/ethregistrar/ETHRegistrarController.sol +/// +/// @author Coinbase (https://github.com/base-org/usernames) +contract UpgradeableRegistrarController is OwnableUpgradeable { + using StringUtils for *; + using SafeERC20 for IERC20; + using EnumerableSetLib for EnumerableSetLib.Bytes32Set; + + /// @notice The details of a registration request. + struct RegisterRequest { + /// @dev The name being registered. + string name; + /// @dev The address of the owner for the name. + address owner; + /// @dev The duration of the registration in seconds. + uint256 duration; + /// @dev The address of the resolver to set for this name. + address resolver; + /// @dev Multicallable data bytes for setting records in the associated resolver upon reigstration. + bytes[] data; + /// @dev Bool to decide whether to set this name as the "primary" name for the `owner`. + bool reverseRecord; + } + + /// @notice The details of a discount tier. + struct DiscountDetails { + /// @dev Bool which declares whether the discount is active or not. + bool active; + /// @dev The address of the associated validator. It must implement `IDiscountValidator`. + address discountValidator; + /// @dev The unique key that identifies this discount. + bytes32 key; + /// @dev The discount value denominated in wei. + uint256 discount; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STORAGE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + struct URCStorage { + /// @notice The implementation of the `BaseRegistrar`. + BaseRegistrar base; + /// @notice The implementation of the pricing oracle. + IPriceOracle prices; + /// @notice The implementation of the Reverse Registrar contract. + IReverseRegistrar reverseRegistrar; + /// @notice An enumerable set for tracking which discounts are currently active. + EnumerableSetLib.Bytes32Set activeDiscounts; + /// @notice The node for which this name enables registration. It must match the `rootNode` of `base`. + bytes32 rootNode; + /// @notice The name for which this registration adds subdomains for, i.e. ".base.eth". + string rootName; + /// @notice The address that will receive ETH funds upon `withdraw()` being called. + address paymentReceiver; + /// @notice The timestamp of "go-live". Used for setting at-launch pricing premium. + uint256 launchTime; + /// @notice The address of the legacy registrar controller + address legacyRegistrarController; + /// @notice Each discount is stored against a unique 32-byte identifier, i.e. keccak256("test.discount.validator"). + mapping(bytes32 key => DiscountDetails details) discounts; + /// @notice Storage for which addresses have already registered with a discount. + mapping(address registrant => bool hasRegisteredWithDiscount) discountedRegistrants; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice The minimum registration duration, specified in seconds. + uint256 public constant MIN_REGISTRATION_DURATION = 365 days; + + /// @notice The minimum name length. + uint256 public constant MIN_NAME_LENGTH = 3; + + /// @notice The storage location + bytes32 private constant UPGRADEABLE_REGISTRAR_CONTROLLER_STORAGE_LOCATION = keccak256(abi.encode(uint256(keccak256("upgradeable.registrar.controller.storage")) - 1)) & ~bytes32(uint256(0xff)); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Thrown when the sender has already registered with a discount. + /// + /// @param sender The address of the sender. + error AlreadyRegisteredWithDiscount(address sender); + + /// @notice Thrown when a name is not available. + /// + /// @param name The name that is not available. + error NameNotAvailable(string name); + + /// @notice Thrown when a name's duration is not longer than `MIN_REGISTRATION_DURATION`. + /// + /// @param duration The duration that was too short. + error DurationTooShort(uint256 duration); + + /// @notice Thrown when Multicallable resolver data was specified but not resolver address was provided. + error ResolverRequiredWhenDataSupplied(); + + /// @notice Thrown when a `discountedRegister` claim tries to access an inactive discount. + /// + /// @param key The discount key that is inactive. + error InactiveDiscount(bytes32 key); + + /// @notice Thrown when the payment received is less than the price. + error InsufficientValue(); + + /// @notice Thrown when the specified discount's validator does not accept the discount for the sender. + /// + /// @param key The discount being accessed. + /// @param data The associated `validationData`. + error InvalidDiscount(bytes32 key, bytes data); + + /// @notice Thrown when the discount amount is 0. + /// + /// @param key The discount being set. + error InvalidDiscountAmount(bytes32 key); + + /// @notice Thrown when the payment receiver is being set to address(0). + error InvalidPaymentReceiver(); + + /// @notice Thrown when the discount validator is being set to address(0). + /// + /// @param key The discount being set. + /// @param validator The address of the validator being set. + error InvalidValidator(bytes32 key, address validator); + + /// @notice Thrown when a refund transfer is unsuccessful. + error TransferFailed(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* EVENTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Emitted when a discount is set or updated. + /// + /// @param discountKey The unique identifier key for the discount. + /// @param details The DiscountDetails struct stored for this key. + event DiscountUpdated(bytes32 indexed discountKey, DiscountDetails details); + + /// @notice Emitted when an ETH payment was processed successfully. + /// + /// @param payee Address that sent the ETH. + /// @param price Value that was paid. + event ETHPaymentProcessed(address indexed payee, uint256 price); + + /// @notice Emitted when a name was registered. + /// + /// @param name The name that was registered. + /// @param label The hashed label of the name. + /// @param owner The owner of the name that was registered. + /// @param expires The date that the registration expires. + event NameRegistered(string name, bytes32 indexed label, address indexed owner, uint256 expires); + + /// @notice Emitted when a name is renewed. + /// + /// @param name The name that was renewed. + /// @param label The hashed label of the name. + /// @param expires The date that the renewed name expires. + event NameRenewed(string name, bytes32 indexed label, uint256 expires); + + /// @notice Emitted when the payment receiver is updated. + /// + /// @param newPaymentReceiver The address of the new payment receiver. + event PaymentReceiverUpdated(address newPaymentReceiver); + + /// @notice Emitted when the price oracle is updated. + /// + /// @param newPrices The address of the new price oracle. + event PriceOracleUpdated(address newPrices); + + /// @notice Emitted when a name is registered with a discount. + /// + /// @param registrant The address of the registrant. + /// @param discountKey The discount key that was used to register. + event DiscountApplied(address indexed registrant, bytes32 indexed discountKey); + + /// @notice Emitted when the reverse registrar is updated. + /// + /// @param newReverseRegistrar The address of the new reverse registrar. + event ReverseRegistrarUpdated(address newReverseRegistrar); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* MODIFIERS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Decorator for validating registration requests. + /// + /// @dev Validates that: + /// 1. There is a `resolver` specified` when `data` is set + /// 2. That the name is `available()` + /// 3. That the registration `duration` is sufficiently long + /// + /// @param request The RegisterRequest that is being validated. + modifier validRegistration(RegisterRequest calldata request) { + if (request.data.length > 0 && request.resolver == address(0)) { + revert ResolverRequiredWhenDataSupplied(); + } + if (!available(request.name)) { + revert NameNotAvailable(request.name); + } + if (request.duration < MIN_REGISTRATION_DURATION) { + revert DurationTooShort(request.duration); + } + _; + } + + /// @notice Decorator for validating discounted registrations. + /// + /// @dev Validates that: + /// 1. That the registrant has not already registered with a discount + /// 2. That the discount is `active` + /// 3. That the associated `discountValidator` returns true when `isValidDiscountRegistration` is called. + /// + /// @param discountKey The uuid of the discount. + /// @param validationData The associated validation data for this discount registration. + modifier validDiscount(bytes32 discountKey, bytes calldata validationData) { + URCStorage storage $ = _getURCStorage(); + if ($.discountedRegistrants[msg.sender]) revert AlreadyRegisteredWithDiscount(msg.sender); + DiscountDetails memory details = $.discounts[discountKey]; + + if (!details.active) revert InactiveDiscount(discountKey); + + IDiscountValidator validator = IDiscountValidator(details.discountValidator); + if (!validator.isValidDiscountRegistration(msg.sender, validationData)) { + revert InvalidDiscount(discountKey, validationData); + } + _; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* IMPLEMENTATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Registrar Controller initialization. + /// + /// @dev Assigns ownership of this contract's reverse record to the `owner_`. + /// + /// @param base_ The base registrar contract. + /// @param prices_ The pricing oracle contract. + /// @param reverseRegistrar_ The reverse registrar contract. + /// @param owner_ The permissioned address initialized as the `owner` in the `Ownable` context. + /// @param rootNode_ The node for which this registrar manages registrations. + /// @param rootName_ The name of the root node which this registrar manages. + function initialize( + BaseRegistrar base_, + IPriceOracle prices_, + IReverseRegistrar reverseRegistrar_, + address owner_, + bytes32 rootNode_, + string memory rootName_, + address paymentReceiver_, + address legacyRegistrarController_ + ) public onlyInitializing { + __Ownable_init(); + transferOwnership(owner_); + + URCStorage storage $ = _getURCStorage(); + $.base = base_; + $.prices = prices_; + $.reverseRegistrar = reverseRegistrar_; + $.rootNode = rootNode_; + $.rootName = rootName_; + $.paymentReceiver = paymentReceiver_; + $.legacyRegistrarController = legacyRegistrarController_; + } + + /// @notice Allows the `owner` to set discount details for a specified `key`. + /// + /// @dev Validates that: + /// 1. The discount `amount` is nonzero + /// 2. The uuid `key` matches the one set in the details + /// 3. That the address of the `discountValidator` is not the zero address + /// Updates the `ActiveDiscounts` enumerable set then emits `DiscountUpdated` event. + /// + /// @param details The DiscountDetails for this discount key. + function setDiscountDetails(DiscountDetails memory details) external onlyOwner { + if (details.discount == 0) revert InvalidDiscountAmount(details.key); + if (details.discountValidator == address(0)) revert InvalidValidator(details.key, details.discountValidator); + _getURCStorage().discounts[details.key] = details; + _updateActiveDiscounts(details.key, details.active); + emit DiscountUpdated(details.key, details); + } + + /// @notice Allows the `owner` to set the pricing oracle contract. + /// + /// @dev Emits `PriceOracleUpdated` after setting the `prices` contract. + /// + /// @param prices_ The new pricing oracle. + function setPriceOracle(IPriceOracle prices_) external onlyOwner { + _getURCStorage().prices = prices_; + emit PriceOracleUpdated(address(prices_)); + } + + /// @notice Allows the `owner` to set the reverse registrar contract. + /// + /// @dev Emits `ReverseRegistrarUpdated` after setting the `reverseRegistrar` contract. + /// + /// @param reverse_ The new reverse registrar contract. + function setReverseRegistrar(IReverseRegistrar reverse_) external onlyOwner { + _getURCStorage().reverseRegistrar = reverse_; + emit ReverseRegistrarUpdated(address(reverse_)); + } + + /// @notice Allows the `owner` to set the stored `launchTime`. + /// + /// @param launchTime_ The new launch time timestamp. + function setLaunchTime(uint256 launchTime_) external onlyOwner { + _getURCStorage().launchTime = launchTime_; + } + + /// @notice Allows the `owner` to set the reverse registrar contract. + /// + /// @dev Emits `PaymentReceiverUpdated` after setting the `paymentReceiver` address. + /// + /// @param paymentReceiver_ The new payment receiver address. + function setPaymentReceiver(address paymentReceiver_) external onlyOwner { + if (paymentReceiver_ == address(0)) revert InvalidPaymentReceiver(); + _getURCStorage().paymentReceiver = paymentReceiver_; + emit PaymentReceiverUpdated(paymentReceiver_); + } + + /// @notice Checks whether any of the provided addresses have registered with a discount. + /// + /// @param addresses The array of addresses to check for discount registration. + /// + /// @return `true` if any of the addresses have already registered with a discount, else `false`. + function hasRegisteredWithDiscount(address[] memory addresses) external view returns (bool) { + URCStorage storage $ = _getURCStorage(); + for (uint256 i; i < addresses.length; i++) { + if ($.discountedRegistrants[addresses[i]]) { + return true; + } + } + return false; + } + + /// @notice Checks whether the provided `name` is long enough. + /// + /// @param name The name to check the length of. + /// + /// @return `true` if the name is equal to or longer than MIN_NAME_LENGTH, else `false`. + function valid(string memory name) public pure returns (bool) { + return name.strlen() >= MIN_NAME_LENGTH; + } + + /// @notice Checks whether the provided `name` is available. + /// + /// @param name The name to check the availability of. + /// + /// @return `true` if the name is `valid` and available on the `base` registrar, else `false`. + function available(string memory name) public view returns (bool) { + bytes32 label = keccak256(bytes(name)); + URCStorage storage $ = _getURCStorage(); + return valid(name) && $.base.isAvailable(uint256(label)); + } + + /// @notice Checks the rent price for a provided `name` and `duration`. + /// + /// @param name The name to check the rent price of. + /// @param duration The time that the name would be rented. + /// + /// @return price The `Price` tuple containing the base and premium prices respectively, denominated in wei. + function rentPrice(string memory name, uint256 duration) public view returns (IPriceOracle.Price memory price) { + bytes32 label = keccak256(bytes(name)); + price = _getURCStorage().prices.price(name, _getExpiry(uint256(label)), duration); + } + + /// @notice Checks the register price for a provided `name` and `duration`. + /// + /// @param name The name to check the register price of. + /// @param duration The time that the name would be registered. + /// + /// @return The all-in price for the name registration, denominated in wei. + function registerPrice(string memory name, uint256 duration) public view returns (uint256) { + IPriceOracle.Price memory price = rentPrice(name, duration); + return price.base + price.premium; + } + + /// @notice Checks the discounted register price for a provided `name`, `duration` and `discountKey`. + /// + /// @dev The associated `DiscountDetails.discount` is subtracted from the price returned by calling `registerPrice()`. + /// + /// @param name The name to check the discounted register price of. + /// @param duration The time that the name would be registered. + /// @param discountKey The uuid of the discount to apply. + /// + /// @return price The all-ing price for the discounted name registration, denominated in wei. Returns 0 + /// if the price of the discount exceeds the nominal registration fee. + function discountedRegisterPrice(string memory name, uint256 duration, bytes32 discountKey) + public + view + returns (uint256 price) + { + URCStorage storage $ = _getURCStorage(); + DiscountDetails memory discount = $.discounts[discountKey]; + price = registerPrice(name, duration); + price = (price >= discount.discount) ? price - discount.discount : 0; + } + + /// @notice Check which discounts are currently set to `active`. + /// + /// @return An array of `DiscountDetails` that are all currently marked as `active`. + function getActiveDiscounts() external view returns (DiscountDetails[] memory) { + URCStorage storage $ = _getURCStorage(); + bytes32[] memory activeDiscountKeys = $.activeDiscounts.values(); + DiscountDetails[] memory activeDiscountDetails = new DiscountDetails[](activeDiscountKeys.length); + for (uint256 i; i < activeDiscountKeys.length; i++) { + activeDiscountDetails[i] = $.discounts[activeDiscountKeys[i]]; + } + return activeDiscountDetails; + } + + /// @notice Enables a caller to register a name. + /// + /// @dev Validates the registration details via the `validRegistration` modifier. + /// This `payable` method must receive appropriate `msg.value` to pass `_validatePayment()`. + /// + /// @param request The `RegisterRequest` struct containing the details for the registration. + function register(RegisterRequest calldata request) public payable validRegistration(request) { + uint256 price = registerPrice(request.name, request.duration); + + _validatePayment(price); + + _register(request); + + _refundExcessEth(price); + } + + /// @notice Enables a caller to register a name and apply a discount. + /// + /// @dev In addition to the validation performed for in a `register` request, this method additionally validates + /// that msg.sender is eligible for the specified `discountKey` given the provided `validationData`. + /// The specific encoding of `validationData` is specified in the implementation of the `discountValidator` + /// that is being called. + /// Emits `RegisteredWithDiscount` upon successful registration. + /// + /// @param request The `RegisterRequest` struct containing the details for the registration. + /// @param discountKey The uuid of the discount being accessed. + /// @param validationData Data necessary to perform the associated discount validation. + function discountedRegister(RegisterRequest calldata request, bytes32 discountKey, bytes calldata validationData) + public + payable + validDiscount(discountKey, validationData) + validRegistration(request) + { + URCStorage storage $ = _getURCStorage(); + + uint256 price = discountedRegisterPrice(request.name, request.duration, discountKey); + + _validatePayment(price); + + $.discountedRegistrants[msg.sender] = true; + _register(request); + + _refundExcessEth(price); + + emit DiscountApplied(msg.sender, discountKey); + } + + /// @notice Allows a caller to renew a name for a specified duration. + /// + /// @dev This `payable` method must receive appropriate `msg.value` to pass `_validatePayment()`. + /// The price for renewal never incorporates pricing `premium`. This is because we only expect + /// renewal on names that are not expired or are in the grace period. Use the `base` price returned + /// by the `rentPrice` tuple to determine the price for calling this method. + /// + /// @param name The name that is being renewed. + /// @param duration The duration to extend the expiry, in seconds. + function renew(string calldata name, uint256 duration) external payable { + URCStorage storage $ = _getURCStorage(); + bytes32 labelhash = keccak256(bytes(name)); + uint256 tokenId = uint256(labelhash); + IPriceOracle.Price memory price = rentPrice(name, duration); + + _validatePayment(price.base); + + uint256 expires = $.base.renew(tokenId, duration); + + _refundExcessEth(price.base); + + emit NameRenewed(name, labelhash, expires); + } + + /// @notice Internal helper for validating ETH payments + /// + /// @dev Emits `ETHPaymentProcessed` after validating the payment. + /// + /// @param price The expected value. + function _validatePayment(uint256 price) internal { + if (msg.value < price) { + revert InsufficientValue(); + } + emit ETHPaymentProcessed(msg.sender, price); + } + + /// @notice Helper for deciding whether to include a launch-premium. + /// + /// @dev If the token returns a `0` expiry time, it hasn't been registered before. On launch, this will be true for all + /// names. Use the `launchTime` to establish a premium price around the actual launch time. + /// + /// @param tokenId The ID of the token to check for expiry. + /// + /// @return expires Returns the expiry + GRACE_PERIOD for previously registered names, else `launchTime`. + function _getExpiry(uint256 tokenId) internal view returns (uint256 expires) { + URCStorage storage $ = _getURCStorage(); + expires = $.base.nameExpires(tokenId); + if (expires == 0) { + return $.launchTime; + } + return expires + GRACE_PERIOD; + } + + /// @notice Shared registartion logic for both `register()` and `discountedRegister()`. + /// + /// @dev Will set records in the specified resolver if the resolver address is non zero and there is `data` in the `request`. + /// Will set the reverse record's owner as msg.sender if `reverseRecord` is `true`. + /// Emits `NameRegistered` upon successful registration. + /// + /// @param request The `RegisterRequest` struct containing the details for the registration. + function _register(RegisterRequest calldata request) internal { + URCStorage storage $ = _getURCStorage(); + uint256 expires = $.base.registerWithRecord( + uint256(keccak256(bytes(request.name))), request.owner, request.duration, request.resolver, 0 + ); + + if (request.data.length > 0) { + _setRecords(request.resolver, keccak256(bytes(request.name)), request.data); + } + + if (request.reverseRecord) { + _setReverseRecord(request.name, request.resolver, msg.sender); + } + + emit NameRegistered(request.name, keccak256(bytes(request.name)), request.owner, expires); + } + + /// @notice Refunds any remaining `msg.value` after processing a registration or renewal given`price`. + /// + /// @dev It is necessary to allow "overpayment" because of premium price decay. We don't want transactions to fail + /// unnecessarily if the premium decreases between tx submission and inclusion. + /// + /// @param price The total value to be retained, denominated in wei. + function _refundExcessEth(uint256 price) internal { + if (msg.value > price) { + (bool sent,) = payable(msg.sender).call{value: (msg.value - price)}(""); + if (!sent) revert TransferFailed(); + } + } + + /// @notice Uses Multicallable to iteratively set records on a specified resolver. + /// + /// @dev `multicallWithNodeCheck` ensures that each record being set is for the specified `label`. + /// + /// @param resolverAddress The address of the resolver to set records on. + /// @param label The keccak256 namehash for the specified name. + /// @param data The abi encoded calldata records that will be used in the multicallable resolver. + function _setRecords(address resolverAddress, bytes32 label, bytes[] calldata data) internal { + URCStorage storage $ = _getURCStorage(); + bytes32 nodehash = keccak256(abi.encodePacked($.rootNode, label)); + L2Resolver resolver = L2Resolver(resolverAddress); + resolver.multicallWithNodeCheck(nodehash, data); + } + + /// @notice Sets the reverse record to `owner` for a specified `name` on the specified `resolver. + /// + /// @param name The specified name. + /// @param resolver The resolver to set the reverse record on. + /// @param owner The owner of the reverse record. + function _setReverseRecord(string memory name, address resolver, address owner) internal { + URCStorage storage $ = _getURCStorage(); + $.reverseRegistrar.setNameForAddr(msg.sender, owner, resolver, string.concat(name, $.rootName)); + } + + /// @notice Helper method for updating the `activeDiscounts` enumerable set. + /// + /// @dev Adds the discount `key` to the set if it is active or removes if it is inactive. + /// + /// @param key The uuid of the discount. + /// @param active Whether the specified discount is active or not. + function _updateActiveDiscounts(bytes32 key, bool active) internal { + URCStorage storage $ = _getURCStorage(); + active ? $.activeDiscounts.add(key) : $.activeDiscounts.remove(key); + } + + /// @notice Allows anyone to withdraw the eth accumulated on this contract back to the `paymentReceiver`. + function withdrawETH() public { + URCStorage storage $ = _getURCStorage(); + (bool sent,) = payable($.paymentReceiver).call{value: (address(this).balance)}(""); + if (!sent) revert TransferFailed(); + } + + /// @notice Allows the owner to recover ERC20 tokens sent to the contract by mistake. + /// + /// @param _to The address to send the tokens to. + /// @param _token The address of the ERC20 token to recover + /// @param _amount The amount of tokens to recover. + function recoverFunds(address _token, address _to, uint256 _amount) external onlyOwner { + IERC20(_token).safeTransfer(_to, _amount); + } + + function _getURCStorage() private pure returns (URCStorage storage $) { + assembly { + $.slot := UPGRADEABLE_REGISTRAR_CONTROLLER_STORAGE_LOCATION + } + } +} From a578e1410b6a32a35e0ef2fa9754b3fe08b9114a Mon Sep 17 00:00:00 2001 From: katzman Date: Tue, 1 Oct 2024 15:35:01 -0700 Subject: [PATCH 2/6] Add interface to new ENS contract, add support for setting primary to new contract --- src/L2/UpgradeableRegistrarController.sol | 27 ++++++++++++++--------- src/L2/interface/IL2ReverseResolver.sol | 19 ++++++++++++++++ 2 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 src/L2/interface/IL2ReverseResolver.sol diff --git a/src/L2/UpgradeableRegistrarController.sol b/src/L2/UpgradeableRegistrarController.sol index 83c86f1..9541570 100644 --- a/src/L2/UpgradeableRegistrarController.sol +++ b/src/L2/UpgradeableRegistrarController.sol @@ -10,9 +10,11 @@ import {StringUtils} from "ens-contracts/ethregistrar/StringUtils.sol"; import {BASE_ETH_NODE, GRACE_PERIOD} from "src/util/Constants.sol"; import {BaseRegistrar} from "./BaseRegistrar.sol"; import {IDiscountValidator} from "./interface/IDiscountValidator.sol"; +import {IL2ReverseResolver} from "./interface/IL2ReverseResolver.sol"; import {IPriceOracle} from "./interface/IPriceOracle.sol"; import {L2Resolver} from "./L2Resolver.sol"; import {IReverseRegistrar} from "./interface/IReverseRegistrar.sol"; +import {RegistrarController} from "./RegistrarController.sol"; /// @title Registrar Controller /// @@ -44,6 +46,10 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { bytes[] data; /// @dev Bool to decide whether to set this name as the "primary" name for the `owner`. bool reverseRecord; + /// @dev Signature expiry + uint256 signatureExpiry; + /// @dev Signature payload + bytes signature; } /// @notice The details of a discount tier. @@ -58,10 +64,6 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { uint256 discount; } - /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ - /* STORAGE */ - /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - struct URCStorage { /// @notice The implementation of the `BaseRegistrar`. BaseRegistrar base; @@ -81,6 +83,8 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { uint256 launchTime; /// @notice The address of the legacy registrar controller address legacyRegistrarController; + /// @notice The address of the L2 Reverse Resolver + address reverseResolver; /// @notice Each discount is stored against a unique 32-byte identifier, i.e. keccak256("test.discount.validator"). mapping(bytes32 key => DiscountDetails details) discounts; /// @notice Storage for which addresses have already registered with a discount. @@ -97,9 +101,9 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { /// @notice The minimum name length. uint256 public constant MIN_NAME_LENGTH = 3; - /// @notice The storage location - bytes32 private constant UPGRADEABLE_REGISTRAR_CONTROLLER_STORAGE_LOCATION = keccak256(abi.encode(uint256(keccak256("upgradeable.registrar.controller.storage")) - 1)) & ~bytes32(uint256(0xff)); - + /// @notice The EIP-7201 storage location, determined by: + /// keccak256(abi.encode(uint256(keccak256("upgradeable.registrar.controller.storage")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant UPGRADEABLE_REGISTRAR_CONTROLLER_STORAGE_LOCATION = 0xf52df153eda7a96204b686efee7d70251f4cef9d04988d95cc73d1a93f655200; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* ERRORS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -358,7 +362,7 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { function hasRegisteredWithDiscount(address[] memory addresses) external view returns (bool) { URCStorage storage $ = _getURCStorage(); for (uint256 i; i < addresses.length; i++) { - if ($.discountedRegistrants[addresses[i]]) { + if ($.discountedRegistrants[addresses[i]] || RegistrarController($.legacyRegistrarController).hasRegisteredWithDiscount(addresses)) { return true; } } @@ -559,7 +563,7 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { } if (request.reverseRecord) { - _setReverseRecord(request.name, request.resolver, msg.sender); + _setReverseRecord(request.name, request.resolver, msg.sender, request.signatureExpiry, request.signature); } emit NameRegistered(request.name, keccak256(bytes(request.name)), request.owner, expires); @@ -597,9 +601,12 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { /// @param name The specified name. /// @param resolver The resolver to set the reverse record on. /// @param owner The owner of the reverse record. - function _setReverseRecord(string memory name, address resolver, address owner) internal { + function _setReverseRecord(string memory name, address resolver, address owner, uint256 expiry, bytes memory signature) internal { URCStorage storage $ = _getURCStorage(); + // vesitigial reverse resolution $.reverseRegistrar.setNameForAddr(msg.sender, owner, resolver, string.concat(name, $.rootName)); + // new reverse resolver + IL2ReverseResolver($.reverseResolver).setNameForAddrWithSignature(msg.sender, name, expiry, signature); } /// @notice Helper method for updating the `activeDiscounts` enumerable set. diff --git a/src/L2/interface/IL2ReverseResolver.sol b/src/L2/interface/IL2ReverseResolver.sol new file mode 100644 index 0000000..af9c6d2 --- /dev/null +++ b/src/L2/interface/IL2ReverseResolver.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +interface IL2ReverseResolver { + /** + * @dev Sets the name for an addr using a signature that can be verified with ERC1271. + * @param addr The reverse record to set + * @param name The name of the reverse record + * @param signatureExpiry Date when the signature expires + * @param signature The resolver of the reverse node + * @return The ENS node hash of the reverse record. + */ + function setNameForAddrWithSignature( + address addr, + string calldata name, + uint256 signatureExpiry, + bytes memory signature + ) external returns (bytes32); +} \ No newline at end of file From 5dfed4afbb9038446189b81029e030e20993beb2 Mon Sep 17 00:00:00 2001 From: katzman Date: Tue, 1 Oct 2024 15:36:46 -0700 Subject: [PATCH 3/6] Add initializer for reverse resolver contract --- src/L2/UpgradeableRegistrarController.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/L2/UpgradeableRegistrarController.sol b/src/L2/UpgradeableRegistrarController.sol index 9541570..8a46d4d 100644 --- a/src/L2/UpgradeableRegistrarController.sol +++ b/src/L2/UpgradeableRegistrarController.sol @@ -284,7 +284,8 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { bytes32 rootNode_, string memory rootName_, address paymentReceiver_, - address legacyRegistrarController_ + address legacyRegistrarController_, + address reverseResolver_ ) public onlyInitializing { __Ownable_init(); transferOwnership(owner_); @@ -297,6 +298,7 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { $.rootName = rootName_; $.paymentReceiver = paymentReceiver_; $.legacyRegistrarController = legacyRegistrarController_; + $.reverseResolver = reverseResolver_; } /// @notice Allows the `owner` to set discount details for a specified `key`. From 43a53a7ba9f97eb10756b3f88ac0f125db154910 Mon Sep 17 00:00:00 2001 From: katzman Date: Tue, 1 Oct 2024 15:56:16 -0700 Subject: [PATCH 4/6] Add shim for migration period --- src/L2/ReverseRegistrarShim.sol | 27 +++++++++++++++++++++++ src/L2/UpgradeableRegistrarController.sol | 18 +++++++++++---- src/L2/interface/IL2ReverseResolver.sol | 2 +- 3 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 src/L2/ReverseRegistrarShim.sol diff --git a/src/L2/ReverseRegistrarShim.sol b/src/L2/ReverseRegistrarShim.sol new file mode 100644 index 0000000..aac7151 --- /dev/null +++ b/src/L2/ReverseRegistrarShim.sol @@ -0,0 +1,27 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IReverseRegistrar} from "./interface/IReverseRegistrar.sol"; +import {IL2ReverseResolver} from "./interface/IL2ReverseResolver.sol"; + +contract ReverseRegistrarShim { + address public immutable reverseRegistrar; + address public immutable reverseResolver; + address public immutable l2Resolver; + + constructor(address reverseRegistrar_, address reverseResolver_, address l2Resolver_) { + reverseRegistrar = reverseRegistrar_; + reverseResolver = reverseResolver_; + l2Resolver = l2Resolver_; + } + + function setNameForAddrWithSignature( + address addr, + string calldata name, + uint256 signatureExpiry, + bytes memory signature + ) external returns (bytes32) { + IReverseRegistrar(reverseRegistrar).setNameForAddr(addr, msg.sender, l2Resolver, name); + return IL2ReverseResolver(reverseResolver).setNameForAddrWithSignature(addr, name, signatureExpiry, signature); + } +} diff --git a/src/L2/UpgradeableRegistrarController.sol b/src/L2/UpgradeableRegistrarController.sol index 8a46d4d..3484ca9 100644 --- a/src/L2/UpgradeableRegistrarController.sol +++ b/src/L2/UpgradeableRegistrarController.sol @@ -103,7 +103,8 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { /// @notice The EIP-7201 storage location, determined by: /// keccak256(abi.encode(uint256(keccak256("upgradeable.registrar.controller.storage")) - 1)) & ~bytes32(uint256(0xff)); - bytes32 private constant UPGRADEABLE_REGISTRAR_CONTROLLER_STORAGE_LOCATION = 0xf52df153eda7a96204b686efee7d70251f4cef9d04988d95cc73d1a93f655200; + bytes32 private constant UPGRADEABLE_REGISTRAR_CONTROLLER_STORAGE_LOCATION = + 0xf52df153eda7a96204b686efee7d70251f4cef9d04988d95cc73d1a93f655200; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* ERRORS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -364,7 +365,10 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { function hasRegisteredWithDiscount(address[] memory addresses) external view returns (bool) { URCStorage storage $ = _getURCStorage(); for (uint256 i; i < addresses.length; i++) { - if ($.discountedRegistrants[addresses[i]] || RegistrarController($.legacyRegistrarController).hasRegisteredWithDiscount(addresses)) { + if ( + $.discountedRegistrants[addresses[i]] + || RegistrarController($.legacyRegistrarController).hasRegisteredWithDiscount(addresses) + ) { return true; } } @@ -603,11 +607,17 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { /// @param name The specified name. /// @param resolver The resolver to set the reverse record on. /// @param owner The owner of the reverse record. - function _setReverseRecord(string memory name, address resolver, address owner, uint256 expiry, bytes memory signature) internal { + function _setReverseRecord( + string memory name, + address resolver, + address owner, + uint256 expiry, + bytes memory signature + ) internal { URCStorage storage $ = _getURCStorage(); // vesitigial reverse resolution $.reverseRegistrar.setNameForAddr(msg.sender, owner, resolver, string.concat(name, $.rootName)); - // new reverse resolver + // new reverse resolver IL2ReverseResolver($.reverseResolver).setNameForAddrWithSignature(msg.sender, name, expiry, signature); } diff --git a/src/L2/interface/IL2ReverseResolver.sol b/src/L2/interface/IL2ReverseResolver.sol index af9c6d2..f08fa5d 100644 --- a/src/L2/interface/IL2ReverseResolver.sol +++ b/src/L2/interface/IL2ReverseResolver.sol @@ -16,4 +16,4 @@ interface IL2ReverseResolver { uint256 signatureExpiry, bytes memory signature ) external returns (bytes32); -} \ No newline at end of file +} From fc735bf29b9f67fee6e27241008d8674cfba62ff Mon Sep 17 00:00:00 2001 From: katzman Date: Wed, 2 Oct 2024 12:02:21 -0700 Subject: [PATCH 5/6] Add tests for Shim --- .../ReverseRegistrarShimBase.t.sol | 36 ++++++++++++++++++ .../SetNameForAddrWithSignature.t.sol | 38 +++++++++++++++++++ test/mocks/MockReverseResolver.sol | 8 ++++ 3 files changed, 82 insertions(+) create mode 100644 test/ReverseRegistrarShim/ReverseRegistrarShimBase.t.sol create mode 100644 test/ReverseRegistrarShim/SetNameForAddrWithSignature.t.sol create mode 100644 test/mocks/MockReverseResolver.sol diff --git a/test/ReverseRegistrarShim/ReverseRegistrarShimBase.t.sol b/test/ReverseRegistrarShim/ReverseRegistrarShimBase.t.sol new file mode 100644 index 0000000..d19b2fe --- /dev/null +++ b/test/ReverseRegistrarShim/ReverseRegistrarShimBase.t.sol @@ -0,0 +1,36 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {ReverseRegistrarShim} from "src/L2/ReverseRegistrarShim.sol"; +import {MockReverseRegistrar} from "test/mocks/MockReverseRegistrar.sol"; +import {MockReverseResolver} from "test/mocks/MockReverseResolver.sol"; +import {MockPublicResolver} from "test/mocks/MockPublicResolver.sol"; + +contract ReverseRegistrarShimBase is Test { + + MockReverseResolver revRes; + MockReverseRegistrar revReg; + MockPublicResolver resolver; + + ReverseRegistrarShim public shim; + + address userA; + address userB; + string nameA = "userAName"; + string nameB = "userBName"; + + uint256 signatureExpiry = 0; + bytes signature; + + function setUp() external { + revRes = new MockReverseResolver(); + revReg = new MockReverseRegistrar(); + resolver = new MockPublicResolver(); + shim = new ReverseRegistrarShim(address(revReg), address(revRes), address(resolver)); + + userA = makeAddr("userA"); + userB = makeAddr("userB"); + + } +} diff --git a/test/ReverseRegistrarShim/SetNameForAddrWithSignature.t.sol b/test/ReverseRegistrarShim/SetNameForAddrWithSignature.t.sol new file mode 100644 index 0000000..17eee48 --- /dev/null +++ b/test/ReverseRegistrarShim/SetNameForAddrWithSignature.t.sol @@ -0,0 +1,38 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {ReverseRegistrarShimBase} from "./ReverseRegistrarShimBase.t.sol"; +import {MockReverseRegistrar} from "test/mocks/MockReverseRegistrar.sol"; +import {MockReverseResolver} from "test/mocks/MockReverseResolver.sol"; + +contract SetNameForAddrWithSignature is ReverseRegistrarShimBase { + function test_setsNameForAddr_onReverseRegistrar() public { + vm.prank(userA); + vm.expectCall( + address(revReg), + abi.encodeWithSelector( + MockReverseRegistrar.setNameForAddr.selector, + userA, + userA, + address(resolver), + nameA + ) + ); + shim.setNameForAddrWithSignature(userA, nameA, signatureExpiry, signature); + } + + function test_setsNameForAddr_onReverseResolver() public { + vm.prank(userA); + vm.expectCall( + address(revRes), + abi.encodeWithSelector( + MockReverseResolver.setNameForAddrWithSignature.selector, + userA, + nameA, + signatureExpiry, + signature + ) + ); + shim.setNameForAddrWithSignature(userA, nameA, signatureExpiry, signature); + } +} \ No newline at end of file diff --git a/test/mocks/MockReverseResolver.sol b/test/mocks/MockReverseResolver.sol new file mode 100644 index 0000000..34981f2 --- /dev/null +++ b/test/mocks/MockReverseResolver.sol @@ -0,0 +1,8 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +contract MockReverseResolver { + function setNameForAddrWithSignature(address, string calldata, uint256, bytes memory) external view returns (bytes32) { + return bytes32(block.timestamp); + } +} \ No newline at end of file From 7a0d04962e22f4b392811f3879d16ea53d5ce357 Mon Sep 17 00:00:00 2001 From: katzman Date: Wed, 2 Oct 2024 12:02:37 -0700 Subject: [PATCH 6/6] lint --- .../ReverseRegistrarShimBase.t.sol | 2 -- .../SetNameForAddrWithSignature.t.sol | 20 +++++-------------- test/mocks/MockReverseResolver.sol | 8 ++++++-- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/test/ReverseRegistrarShim/ReverseRegistrarShimBase.t.sol b/test/ReverseRegistrarShim/ReverseRegistrarShimBase.t.sol index d19b2fe..f990099 100644 --- a/test/ReverseRegistrarShim/ReverseRegistrarShimBase.t.sol +++ b/test/ReverseRegistrarShim/ReverseRegistrarShimBase.t.sol @@ -8,7 +8,6 @@ import {MockReverseResolver} from "test/mocks/MockReverseResolver.sol"; import {MockPublicResolver} from "test/mocks/MockPublicResolver.sol"; contract ReverseRegistrarShimBase is Test { - MockReverseResolver revRes; MockReverseRegistrar revReg; MockPublicResolver resolver; @@ -31,6 +30,5 @@ contract ReverseRegistrarShimBase is Test { userA = makeAddr("userA"); userB = makeAddr("userB"); - } } diff --git a/test/ReverseRegistrarShim/SetNameForAddrWithSignature.t.sol b/test/ReverseRegistrarShim/SetNameForAddrWithSignature.t.sol index 17eee48..751cd0e 100644 --- a/test/ReverseRegistrarShim/SetNameForAddrWithSignature.t.sol +++ b/test/ReverseRegistrarShim/SetNameForAddrWithSignature.t.sol @@ -9,14 +9,8 @@ contract SetNameForAddrWithSignature is ReverseRegistrarShimBase { function test_setsNameForAddr_onReverseRegistrar() public { vm.prank(userA); vm.expectCall( - address(revReg), - abi.encodeWithSelector( - MockReverseRegistrar.setNameForAddr.selector, - userA, - userA, - address(resolver), - nameA - ) + address(revReg), + abi.encodeWithSelector(MockReverseRegistrar.setNameForAddr.selector, userA, userA, address(resolver), nameA) ); shim.setNameForAddrWithSignature(userA, nameA, signatureExpiry, signature); } @@ -24,15 +18,11 @@ contract SetNameForAddrWithSignature is ReverseRegistrarShimBase { function test_setsNameForAddr_onReverseResolver() public { vm.prank(userA); vm.expectCall( - address(revRes), + address(revRes), abi.encodeWithSelector( - MockReverseResolver.setNameForAddrWithSignature.selector, - userA, - nameA, - signatureExpiry, - signature + MockReverseResolver.setNameForAddrWithSignature.selector, userA, nameA, signatureExpiry, signature ) ); shim.setNameForAddrWithSignature(userA, nameA, signatureExpiry, signature); } -} \ No newline at end of file +} diff --git a/test/mocks/MockReverseResolver.sol b/test/mocks/MockReverseResolver.sol index 34981f2..a233f0a 100644 --- a/test/mocks/MockReverseResolver.sol +++ b/test/mocks/MockReverseResolver.sol @@ -2,7 +2,11 @@ pragma solidity ^0.8.23; contract MockReverseResolver { - function setNameForAddrWithSignature(address, string calldata, uint256, bytes memory) external view returns (bytes32) { + function setNameForAddrWithSignature(address, string calldata, uint256, bytes memory) + external + view + returns (bytes32) + { return bytes32(block.timestamp); } -} \ No newline at end of file +}