From c307e7842b33b7fee1644b3f44f7c361d0a12346 Mon Sep 17 00:00:00 2001 From: katzman Date: Tue, 1 Oct 2024 13:54:56 -0700 Subject: [PATCH 01/13] 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 02/13] 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 03/13] 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 04/13] 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 05/13] 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 06/13] 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 +} From 81513023c178981c7f6ff0664b6ff001271d3b25 Mon Sep 17 00:00:00 2001 From: katzman Date: Thu, 10 Oct 2024 14:54:42 -0700 Subject: [PATCH 07/13] Testing start --- src/L2/UpgradeableRegistrarController.sol | 52 +++++-- .../Available.t.sol | 21 +++ .../DiscountedRegister.t.sol | 144 ++++++++++++++++++ .../DiscountedRegisterPrice.t.sol | 29 ++++ .../RecoverFunds.t.sol | 35 +++++ .../Register.t.sol | 86 +++++++++++ .../RegisterPrice.t.sol | 21 +++ .../Renew.t.sol | 46 ++++++ .../RentPrice.t.sol | 31 ++++ .../SetDiscountDetails.t.sol | 62 ++++++++ .../SetPaymentReceiver.t.sol | 30 ++++ .../SetPriceOracle.t.sol | 27 ++++ .../SetReverseRegistrar.t.sol | 27 ++++ .../UpgradeableRegistrarControllerBase.t.sol | 118 ++++++++++++++ .../Valid.t.sol | 18 +++ .../WithdrawETH.t.sol | 16 ++ test/mocks/MockRegistrarController.sol | 24 +++ 17 files changed, 771 insertions(+), 16 deletions(-) create mode 100644 test/UpgradeableRegistrarController/Available.t.sol create mode 100644 test/UpgradeableRegistrarController/DiscountedRegister.t.sol create mode 100644 test/UpgradeableRegistrarController/DiscountedRegisterPrice.t.sol create mode 100644 test/UpgradeableRegistrarController/RecoverFunds.t.sol create mode 100644 test/UpgradeableRegistrarController/Register.t.sol create mode 100644 test/UpgradeableRegistrarController/RegisterPrice.t.sol create mode 100644 test/UpgradeableRegistrarController/Renew.t.sol create mode 100644 test/UpgradeableRegistrarController/RentPrice.t.sol create mode 100644 test/UpgradeableRegistrarController/SetDiscountDetails.t.sol create mode 100644 test/UpgradeableRegistrarController/SetPaymentReceiver.t.sol create mode 100644 test/UpgradeableRegistrarController/SetPriceOracle.t.sol create mode 100644 test/UpgradeableRegistrarController/SetReverseRegistrar.t.sol create mode 100644 test/UpgradeableRegistrarController/UpgradeableRegistrarControllerBase.t.sol create mode 100644 test/UpgradeableRegistrarController/Valid.t.sol create mode 100644 test/UpgradeableRegistrarController/WithdrawETH.t.sol create mode 100644 test/mocks/MockRegistrarController.sol diff --git a/src/L2/UpgradeableRegistrarController.sol b/src/L2/UpgradeableRegistrarController.sol index 3484ca9..ed28e06 100644 --- a/src/L2/UpgradeableRegistrarController.sol +++ b/src/L2/UpgradeableRegistrarController.sol @@ -287,7 +287,7 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { address paymentReceiver_, address legacyRegistrarController_, address reverseResolver_ - ) public onlyInitializing { + ) public initializer onlyInitializing { __Ownable_init(); transferOwnership(owner_); @@ -300,6 +300,7 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { $.paymentReceiver = paymentReceiver_; $.legacyRegistrarController = legacyRegistrarController_; $.reverseResolver = reverseResolver_; + $.launchTime = RegistrarController(legacyRegistrarController_).launchTime(); } /// @notice Allows the `owner` to set discount details for a specified `key`. @@ -339,13 +340,6 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { 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. @@ -391,8 +385,37 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { /// @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)); + return valid(name) && _getURCStorage().base.isAvailable(uint256(label)); + } + + /// @notice Fetches a specific discount from storage. + /// + /// @param discountKey The uuid of the discount to fetch. + /// + /// @return DiscountDetails associated with the provided `discountKey`. + function discounts(bytes32 discountKey) external view returns (DiscountDetails memory) { + return _getURCStorage().discounts[discountKey]; + } + + /// @notice Fetches the payment receiver from storage.abi + /// + /// @return The address of the payment receiver. + function paymentReceiver() external view returns (address) { + return _getURCStorage().paymentReceiver; + } + + /// @notice Fetches the price oracle from storage. + /// + /// @return The stored prices oracle. + function prices() external view returns (IPriceOracle) { + return _getURCStorage().prices; + } + + /// @notice Fetches the Reverse Registrar from storage. + /// + /// @return The stored Reverse Registrar. + function reverseRegistrar() external view returns (IReverseRegistrar) { + return _getURCStorage().reverseRegistrar; } /// @notice Checks the rent price for a provided `name` and `duration`. @@ -559,8 +582,7 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { /// /// @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 expires = _getURCStorage().base.registerWithRecord( uint256(keccak256(bytes(request.name))), request.owner, request.duration, request.resolver, 0 ); @@ -596,8 +618,7 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { /// @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)); + bytes32 nodehash = keccak256(abi.encodePacked(_getURCStorage().rootNode, label)); L2Resolver resolver = L2Resolver(resolverAddress); resolver.multicallWithNodeCheck(nodehash, data); } @@ -634,8 +655,7 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { /// @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)}(""); + (bool sent,) = payable(_getURCStorage().paymentReceiver).call{value: (address(this).balance)}(""); if (!sent) revert TransferFailed(); } diff --git a/test/UpgradeableRegistrarController/Available.t.sol b/test/UpgradeableRegistrarController/Available.t.sol new file mode 100644 index 0000000..a2bbfbd --- /dev/null +++ b/test/UpgradeableRegistrarController/Available.t.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControllerBase.t.sol"; + +contract Available is UpgradeableRegistrarControllerBase { + function test_returnsFalse_whenNotAvailableOnBase() public { + base.setAvailable(uint256(nameLabel), false); + assertFalse(controller.available(name)); + } + + function test_returnsFalse_whenInvalidLength() public { + base.setAvailable(uint256(shortNameLabel), true); + assertFalse(controller.available(shortName)); + } + + function test_returnsTrue_whenValidAndAvailable() public { + base.setAvailable(uint256(nameLabel), true); + assertTrue(controller.available(name)); + } +} diff --git a/test/UpgradeableRegistrarController/DiscountedRegister.t.sol b/test/UpgradeableRegistrarController/DiscountedRegister.t.sol new file mode 100644 index 0000000..b16483c --- /dev/null +++ b/test/UpgradeableRegistrarController/DiscountedRegister.t.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControllerBase.t.sol"; +import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; +import {IPriceOracle} from "src/L2/interface/IPriceOracle.sol"; + +contract DiscountedRegister is UpgradeableRegistrarControllerBase { + function test_reverts_ifTheDiscountIsInactive() public { + UpgradeableRegistrarController.DiscountDetails memory inactiveDiscount = _getDefaultDiscount(); + vm.deal(user, 1 ether); + + inactiveDiscount.active = false; + vm.prank(owner); + controller.setDiscountDetails(inactiveDiscount); + uint256 price = controller.discountedRegisterPrice(name, duration, discountKey); + + vm.expectRevert(abi.encodeWithSelector(UpgradeableRegistrarController.InactiveDiscount.selector, discountKey)); + vm.prank(user); + controller.discountedRegister{value: price}(_getDefaultRegisterRequest(), discountKey, ""); + } + + function test_reverts_whenInvalidDiscountRegistration() public { + vm.deal(user, 1 ether); + vm.prank(owner); + controller.setDiscountDetails(_getDefaultDiscount()); + validator.setReturnValue(false); + uint256 price = controller.discountedRegisterPrice(name, duration, discountKey); + + vm.expectRevert(abi.encodeWithSelector(UpgradeableRegistrarController.InvalidDiscount.selector, discountKey, "")); + vm.prank(user); + controller.discountedRegister{value: price}(_getDefaultRegisterRequest(), discountKey, ""); + } + + function test_reverts_whenNameNotAvailble() public { + vm.deal(user, 1 ether); + vm.prank(owner); + controller.setDiscountDetails(_getDefaultDiscount()); + uint256 price = controller.discountedRegisterPrice(name, duration, discountKey); + validator.setReturnValue(true); + base.setAvailable(uint256(nameLabel), false); + + vm.expectRevert(abi.encodeWithSelector(UpgradeableRegistrarController.NameNotAvailable.selector, name)); + vm.prank(user); + controller.discountedRegister{value: price}(_getDefaultRegisterRequest(), discountKey, ""); + } + + function test_reverts_whenDurationTooShort() public { + vm.deal(user, 1 ether); + vm.prank(owner); + controller.setDiscountDetails(_getDefaultDiscount()); + uint256 price = controller.discountedRegisterPrice(name, duration, discountKey); + validator.setReturnValue(true); + base.setAvailable(uint256(nameLabel), true); + + UpgradeableRegistrarController.RegisterRequest memory shortDurationRequest = _getDefaultRegisterRequest(); + uint256 shortDuration = controller.MIN_REGISTRATION_DURATION() - 1; + shortDurationRequest.duration = shortDuration; + vm.expectRevert(abi.encodeWithSelector(UpgradeableRegistrarController.DurationTooShort.selector, shortDuration)); + vm.prank(user); + controller.discountedRegister{value: price}(shortDurationRequest, discountKey, ""); + } + + function test_reverts_whenValueTooSmall() public { + vm.deal(user, 1 ether); + vm.prank(owner); + controller.setDiscountDetails(_getDefaultDiscount()); + prices.setPrice(name, IPriceOracle.Price({base: 1 ether, premium: 0})); + uint256 price = controller.discountedRegisterPrice(name, duration, discountKey); + validator.setReturnValue(true); + base.setAvailable(uint256(nameLabel), true); + + vm.expectRevert(UpgradeableRegistrarController.InsufficientValue.selector); + vm.prank(user); + controller.discountedRegister{value: price - 1}(_getDefaultRegisterRequest(), discountKey, ""); + } + + function test_registersWithDiscountSuccessfully() public { + vm.deal(user, 1 ether); + vm.prank(owner); + controller.setDiscountDetails(_getDefaultDiscount()); + validator.setReturnValue(true); + base.setAvailable(uint256(nameLabel), true); + UpgradeableRegistrarController.RegisterRequest memory request = _getDefaultRegisterRequest(); + uint256 expires = block.timestamp + request.duration; + base.setNameExpires(uint256(nameLabel), expires); + uint256 price = controller.discountedRegisterPrice(name, duration, discountKey); + + vm.expectEmit(address(controller)); + emit UpgradeableRegistrarController.ETHPaymentProcessed(user, price); + vm.expectEmit(address(controller)); + emit UpgradeableRegistrarController.NameRegistered(request.name, nameLabel, user, expires); + vm.expectEmit(address(controller)); + emit UpgradeableRegistrarController.DiscountApplied(user, discountKey); + + vm.prank(user); + controller.discountedRegister{value: price}(request, discountKey, ""); + + bytes memory retByte = resolver.firstBytes(); + assertEq(keccak256(retByte), keccak256(request.data[0])); + assertTrue(reverse.hasClaimed(user)); + address[] memory addrs = new address[](1); + addrs[0] = user; + assertTrue(controller.hasRegisteredWithDiscount(addrs)); + } + + function test_sendsARefund_ifUserOverpayed() public { + vm.deal(user, 1 ether); + vm.prank(owner); + controller.setDiscountDetails(_getDefaultDiscount()); + validator.setReturnValue(true); + base.setAvailable(uint256(nameLabel), true); + UpgradeableRegistrarController.RegisterRequest memory request = _getDefaultRegisterRequest(); + uint256 expires = block.timestamp + request.duration; + base.setNameExpires(uint256(nameLabel), expires); + uint256 price = controller.discountedRegisterPrice(name, duration, discountKey); + + vm.prank(user); + controller.discountedRegister{value: price + 1}(request, discountKey, ""); + + uint256 expectedBalance = 1 ether - price; + assertEq(user.balance, expectedBalance); + } + + function test_reverts_ifTheRegistrantHasAlreadyRegisteredWithDiscount() public { + vm.deal(user, 1 ether); + vm.prank(owner); + controller.setDiscountDetails(_getDefaultDiscount()); + validator.setReturnValue(true); + base.setAvailable(uint256(nameLabel), true); + UpgradeableRegistrarController.RegisterRequest memory request = _getDefaultRegisterRequest(); + uint256 expires = block.timestamp + request.duration; + base.setNameExpires(uint256(nameLabel), expires); + uint256 price = controller.discountedRegisterPrice(name, duration, discountKey); + + vm.prank(user); + controller.discountedRegister{value: price}(request, discountKey, ""); + + vm.expectRevert(abi.encodeWithSelector(UpgradeableRegistrarController.AlreadyRegisteredWithDiscount.selector, user)); + request.name = "newname"; + vm.prank(user); + controller.discountedRegister{value: price}(request, discountKey, ""); + } +} diff --git a/test/UpgradeableRegistrarController/DiscountedRegisterPrice.t.sol b/test/UpgradeableRegistrarController/DiscountedRegisterPrice.t.sol new file mode 100644 index 0000000..b149922 --- /dev/null +++ b/test/UpgradeableRegistrarController/DiscountedRegisterPrice.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControllerBase.t.sol"; +import {RegistrarController} from "src/L2/RegistrarController.sol"; +import {IPriceOracle} from "src/L2/interface/IPriceOracle.sol"; + +contract DiscountedRegisterPrice is UpgradeableRegistrarControllerBase { + function test_returnsADiscountedPrice_whenThePriceIsGreaterThanTheDiscount(uint256 price) public { + vm.assume(price > discountAmount); + prices.setPrice(name, IPriceOracle.Price({base: price, premium: 0})); + vm.prank(owner); + controller.setDiscountDetails(_getDefaultDiscount()); + + uint256 expectedPrice = price - discountAmount; + uint256 retPrice = controller.discountedRegisterPrice(name, duration, discountKey); + assertEq(retPrice, expectedPrice); + } + + function test_returnsZero_whenThePriceIsLessThanOrEqualToTheDiscount(uint256 price) public { + vm.assume(price > 0 && price <= discountAmount); + prices.setPrice(name, IPriceOracle.Price({base: price, premium: 0})); + vm.prank(owner); + controller.setDiscountDetails(_getDefaultDiscount()); + + uint256 retPrice = controller.discountedRegisterPrice(name, duration, discountKey); + assertEq(retPrice, 0); + } +} diff --git a/test/UpgradeableRegistrarController/RecoverFunds.t.sol b/test/UpgradeableRegistrarController/RecoverFunds.t.sol new file mode 100644 index 0000000..c7517b6 --- /dev/null +++ b/test/UpgradeableRegistrarController/RecoverFunds.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControllerBase.t.sol"; +import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; +import {IPriceOracle} from "src/L2/interface/IPriceOracle.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; +import {MockUSDC} from "test/mocks/MockUSDC.sol"; + +contract RecoverFunds is UpgradeableRegistrarControllerBase { + MockUSDC public usdc; + + function test_reverts_ifCalledByNonOwner(address caller, uint256 amount) public { + vm.assume(caller != owner); + vm.assume(amount > 0 && amount < type(uint128).max); + vm.expectRevert(Ownable.Unauthorized.selector); + vm.prank(caller); + controller.recoverFunds(address(usdc), caller, amount); + } + + function test_allowsTheOwnerToRecoverFunds(uint256 amount) public { + vm.assume(amount > 0 && amount < type(uint128).max); + _setupTokenAndAssignBalanceToController(amount); + assertEq(usdc.balanceOf(owner), 0); + + vm.prank(owner); + controller.recoverFunds(address(usdc), owner, amount); + assertEq(usdc.balanceOf(owner), amount); + } + + function _setupTokenAndAssignBalanceToController(uint256 balance) internal { + usdc = new MockUSDC(); + usdc.mint(address(controller), balance); + } +} diff --git a/test/UpgradeableRegistrarController/Register.t.sol b/test/UpgradeableRegistrarController/Register.t.sol new file mode 100644 index 0000000..607fdcc --- /dev/null +++ b/test/UpgradeableRegistrarController/Register.t.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControllerBase.t.sol"; +import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; +import {IPriceOracle} from "src/L2/interface/IPriceOracle.sol"; + +contract Register is UpgradeableRegistrarControllerBase { + function test_reverts_whenResolverRequiredAndNotSupplied() public { + vm.deal(user, 1 ether); + uint256 price = controller.registerPrice(name, duration); + vm.expectRevert(UpgradeableRegistrarController.ResolverRequiredWhenDataSupplied.selector); + vm.prank(user); + UpgradeableRegistrarController.RegisterRequest memory noResolverRequest = _getDefaultRegisterRequest(); + noResolverRequest.resolver = address(0); + controller.register{value: price}(noResolverRequest); + } + + function test_reverts_whenNameNotAvailble() public { + vm.deal(user, 1 ether); + uint256 price = controller.registerPrice(name, duration); + base.setAvailable(uint256(nameLabel), false); + vm.expectRevert(abi.encodeWithSelector(UpgradeableRegistrarController.NameNotAvailable.selector, name)); + vm.prank(user); + controller.register{value: price}(_getDefaultRegisterRequest()); + } + + function test_reverts_whenDurationTooShort() public { + vm.deal(user, 1 ether); + uint256 price = controller.registerPrice(name, duration); + base.setAvailable(uint256(nameLabel), true); + UpgradeableRegistrarController.RegisterRequest memory shortDurationRequest = _getDefaultRegisterRequest(); + uint256 shortDuration = controller.MIN_REGISTRATION_DURATION() - 1; + shortDurationRequest.duration = shortDuration; + vm.expectRevert(abi.encodeWithSelector(UpgradeableRegistrarController.DurationTooShort.selector, shortDuration)); + vm.prank(user); + controller.register{value: price}(shortDurationRequest); + } + + function test_reverts_whenValueTooSmall() public { + vm.deal(user, 1 ether); + uint256 price = controller.registerPrice(name, duration); + base.setAvailable(uint256(nameLabel), true); + vm.expectRevert(UpgradeableRegistrarController.InsufficientValue.selector); + vm.prank(user); + controller.register{value: price - 1}(_getDefaultRegisterRequest()); + } + + function test_registersSuccessfully() public { + vm.deal(user, 1 ether); + UpgradeableRegistrarController.RegisterRequest memory request = _getDefaultRegisterRequest(); + + base.setAvailable(uint256(nameLabel), true); + uint256 expires = block.timestamp + request.duration; + base.setNameExpires(uint256(nameLabel), expires); + uint256 price = controller.registerPrice(request.name, request.duration); + + vm.expectEmit(address(controller)); + emit UpgradeableRegistrarController.ETHPaymentProcessed(user, price); + vm.expectEmit(address(controller)); + emit UpgradeableRegistrarController.NameRegistered(request.name, nameLabel, user, expires); + + vm.prank(user); + controller.register{value: price}(request); + + bytes memory retByte = resolver.firstBytes(); + assertEq(keccak256(retByte), keccak256(request.data[0])); + assertTrue(reverse.hasClaimed(user)); + } + + function test_sendsARefund_ifUserOverpayed() public { + vm.deal(user, 1 ether); + UpgradeableRegistrarController.RegisterRequest memory request = _getDefaultRegisterRequest(); + + base.setAvailable(uint256(nameLabel), true); + uint256 expires = block.timestamp + request.duration; + base.setNameExpires(uint256(nameLabel), expires); + uint256 price = controller.registerPrice(request.name, request.duration); + + vm.prank(user); + controller.register{value: price + 1}(request); + + uint256 expectedBalance = 1 ether - price; + assertEq(user.balance, expectedBalance); + } +} diff --git a/test/UpgradeableRegistrarController/RegisterPrice.t.sol b/test/UpgradeableRegistrarController/RegisterPrice.t.sol new file mode 100644 index 0000000..5eb2e9e --- /dev/null +++ b/test/UpgradeableRegistrarController/RegisterPrice.t.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControllerBase.t.sol"; +import {IPriceOracle} from "src/L2/interface/IPriceOracle.sol"; + +contract RegisterPrice is UpgradeableRegistrarControllerBase { + function test_returnsRegisterPrice_fromPricingOracle() public view { + uint256 retPrice = controller.registerPrice(name, 0); + assertEq(retPrice, prices.DEFAULT_BASE_WEI() + prices.DEFAULT_PERMIUM_WEI()); + } + + function test_fuzz_returnsRegisterPrice_fromPricingOracle(uint256 fuzzBase, uint256 fuzzPremium) public { + vm.assume(fuzzBase != 0 && fuzzBase < type(uint128).max); + vm.assume(fuzzPremium < type(uint128).max); + IPriceOracle.Price memory expectedPrice = IPriceOracle.Price({base: fuzzBase, premium: fuzzPremium}); + prices.setPrice(name, expectedPrice); + uint256 retPrice = controller.registerPrice(name, 0); + assertEq(retPrice, expectedPrice.base + expectedPrice.premium); + } +} diff --git a/test/UpgradeableRegistrarController/Renew.t.sol b/test/UpgradeableRegistrarController/Renew.t.sol new file mode 100644 index 0000000..846b167 --- /dev/null +++ b/test/UpgradeableRegistrarController/Renew.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControllerBase.t.sol"; +import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; +import {IPriceOracle} from "src/L2/interface/IPriceOracle.sol"; + +contract Renew is UpgradeableRegistrarControllerBase { + function test_allowsAUserToRenewTheirName() public { + vm.deal(user, 1 ether); + (uint256 expires,) = _register(); + IPriceOracle.Price memory price = controller.rentPrice(name, duration); + uint256 newExpiry = expires + duration; + + vm.expectEmit(address(controller)); + emit UpgradeableRegistrarController.ETHPaymentProcessed(user, price.base); + vm.expectEmit(address(controller)); + emit UpgradeableRegistrarController.NameRenewed(name, nameLabel, newExpiry); + + vm.prank(user); + controller.renew{value: price.base}(name, duration); + } + + function test_refundsExcessETH_onOverpaidRenewal() public { + vm.deal(user, 1 ether); + (, uint256 registerPrice) = _register(); + IPriceOracle.Price memory price = controller.rentPrice(name, duration); + + vm.prank(user); + controller.renew{value: (price.base + 1)}(name, duration); + + uint256 expectedBalance = 1 ether - registerPrice - price.base; + assertEq(user.balance, expectedBalance); + } + + function _register() internal returns (uint256, uint256) { + UpgradeableRegistrarController.RegisterRequest memory request = _getDefaultRegisterRequest(); + uint256 price = controller.registerPrice(request.name, request.duration); + base.setAvailable(uint256(nameLabel), true); + uint256 expires = block.timestamp + request.duration; + base.setNameExpires(uint256(nameLabel), expires); + vm.prank(user); + controller.register{value: price}(request); + return (expires, price); + } +} diff --git a/test/UpgradeableRegistrarController/RentPrice.t.sol b/test/UpgradeableRegistrarController/RentPrice.t.sol new file mode 100644 index 0000000..c90a37b --- /dev/null +++ b/test/UpgradeableRegistrarController/RentPrice.t.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControllerBase.t.sol"; +import {IPriceOracle} from "src/L2/interface/IPriceOracle.sol"; + +contract RentPrice is UpgradeableRegistrarControllerBase { + function test_returnsPrice_fromPricingOracle() public view { + IPriceOracle.Price memory retPrices = controller.rentPrice(name, 0); + assertEq(retPrices.base, prices.DEFAULT_BASE_WEI()); + assertEq(retPrices.premium, prices.DEFAULT_PERMIUM_WEI()); + } + + function test_returnsPremium_ifTimeIsNearLaunchTime() public { + vm.prank(owner); + vm.warp(launchTime + 1); + IPriceOracle.Price memory retPrices = controller.rentPrice(name, 0); + assertEq(retPrices.base, prices.DEFAULT_BASE_WEI()); + assertEq(retPrices.premium, prices.DEFAULT_INCLUDED_PREMIUM()); + } + + function test_fuzz_returnsPrice_fromPricingOracle(uint256 fuzzBase, uint256 fuzzPremium) public { + vm.assume(fuzzBase != 0 && fuzzBase < type(uint128).max); + vm.assume(fuzzPremium < type(uint128).max); + IPriceOracle.Price memory expectedPrice = IPriceOracle.Price({base: fuzzBase, premium: fuzzPremium}); + prices.setPrice(name, expectedPrice); + IPriceOracle.Price memory retPrices = controller.rentPrice(name, 0); + assertEq(retPrices.base, expectedPrice.base); + assertEq(retPrices.premium, expectedPrice.premium); + } +} diff --git a/test/UpgradeableRegistrarController/SetDiscountDetails.t.sol b/test/UpgradeableRegistrarController/SetDiscountDetails.t.sol new file mode 100644 index 0000000..ac47a4e --- /dev/null +++ b/test/UpgradeableRegistrarController/SetDiscountDetails.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControllerBase.t.sol"; +import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; + +contract SetDiscountDetails is UpgradeableRegistrarControllerBase { + function test_reverts_ifCalledByNonOwner(address caller) public { + vm.assume(caller != owner); + vm.expectRevert(Ownable.Unauthorized.selector); + vm.prank(caller); + controller.setDiscountDetails(_getDefaultDiscount()); + } + + function test_reverts_ifTheDiscountIsZero() public { + UpgradeableRegistrarController.DiscountDetails memory noDiscount = _getDefaultDiscount(); + noDiscount.discount = 0; + vm.expectRevert(abi.encodeWithSelector(UpgradeableRegistrarController.InvalidDiscountAmount.selector, discountKey)); + vm.prank(owner); + controller.setDiscountDetails(noDiscount); + } + + function test_reverts_ifTheDiscounValidatorIsInvalid() public { + UpgradeableRegistrarController.DiscountDetails memory noValidator = _getDefaultDiscount(); + noValidator.discountValidator = address(0); + vm.expectRevert(abi.encodeWithSelector(UpgradeableRegistrarController.InvalidValidator.selector, discountKey, address(0))); + vm.prank(owner); + controller.setDiscountDetails(noValidator); + } + + function test_setsTheDetailsAccordingly() public { + vm.expectEmit(address(controller)); + emit UpgradeableRegistrarController.DiscountUpdated(discountKey, _getDefaultDiscount()); + vm.prank(owner); + controller.setDiscountDetails(_getDefaultDiscount()); + UpgradeableRegistrarController.DiscountDetails memory discount = controller.discounts(discountKey); + assertTrue(discount.active); + assertEq(discount.discountValidator, address(validator)); + assertEq(discount.key, discountKey); + assertEq(discount.discount, discountAmount); + } + + function test_addsAndRemoves_fromActiveDiscounts() public { + UpgradeableRegistrarController.DiscountDetails memory discountDetails = _getDefaultDiscount(); + + vm.prank(owner); + controller.setDiscountDetails(discountDetails); + UpgradeableRegistrarController.DiscountDetails[] memory activeDiscountsWithActive = controller.getActiveDiscounts(); + assertEq(activeDiscountsWithActive.length, 1); + assertTrue(activeDiscountsWithActive[0].active); + assertEq(activeDiscountsWithActive[0].discountValidator, address(validator)); + assertEq(activeDiscountsWithActive[0].key, discountKey); + assertEq(activeDiscountsWithActive[0].discount, discountAmount); + + discountDetails.active = false; + vm.prank(owner); + controller.setDiscountDetails(discountDetails); + UpgradeableRegistrarController.DiscountDetails[] memory activeDiscountsNoneActive = controller.getActiveDiscounts(); + assertEq(activeDiscountsNoneActive.length, 0); + } +} diff --git a/test/UpgradeableRegistrarController/SetPaymentReceiver.t.sol b/test/UpgradeableRegistrarController/SetPaymentReceiver.t.sol new file mode 100644 index 0000000..32f6010 --- /dev/null +++ b/test/UpgradeableRegistrarController/SetPaymentReceiver.t.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControllerBase.t.sol"; +import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; + +contract SetPaymentReceiver is UpgradeableRegistrarControllerBase { + function test_reverts_ifCalledByNonOwner(address caller) public { + vm.assume(caller != owner && caller != address(0)); + vm.expectRevert(Ownable.Unauthorized.selector); + vm.prank(caller); + controller.setPaymentReceiver(caller); + } + + function test_reverts_ifNewPaymentReceiver_isZeroAddress() public { + vm.expectRevert(UpgradeableRegistrarController.InvalidPaymentReceiver.selector); + vm.prank(owner); + controller.setPaymentReceiver(address(0)); + } + + function test_allowsTheOwner_toSetThePaymentReceiver(address newReceiver) public { + vm.assume(newReceiver != address(0)); + vm.expectEmit(address(controller)); + emit UpgradeableRegistrarController.PaymentReceiverUpdated(newReceiver); + vm.prank(owner); + controller.setPaymentReceiver(newReceiver); + assertEq(newReceiver, controller.paymentReceiver()); + } +} diff --git a/test/UpgradeableRegistrarController/SetPriceOracle.t.sol b/test/UpgradeableRegistrarController/SetPriceOracle.t.sol new file mode 100644 index 0000000..b8bc395 --- /dev/null +++ b/test/UpgradeableRegistrarController/SetPriceOracle.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControllerBase.t.sol"; +import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; +import {MockPriceOracle} from "test/mocks/MockPriceOracle.sol"; +import {IPriceOracle} from "src/L2/interface/IPriceOracle.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; + +contract SetPriceOracle is UpgradeableRegistrarControllerBase { + function test_reverts_ifCalledByNonOwner(address caller) public { + vm.assume(caller != owner); + MockPriceOracle newPrices = new MockPriceOracle(); + vm.expectRevert(Ownable.Unauthorized.selector); + vm.prank(caller); + controller.setPriceOracle(IPriceOracle(address(newPrices))); + } + + function test_setsThePriceOracleAccordingly() public { + vm.expectEmit(); + MockPriceOracle newPrices = new MockPriceOracle(); + emit UpgradeableRegistrarController.PriceOracleUpdated(address(newPrices)); + vm.prank(owner); + controller.setPriceOracle(IPriceOracle(address(newPrices))); + assertEq(address(controller.prices()), address(newPrices)); + } +} diff --git a/test/UpgradeableRegistrarController/SetReverseRegistrar.t.sol b/test/UpgradeableRegistrarController/SetReverseRegistrar.t.sol new file mode 100644 index 0000000..399ca6f --- /dev/null +++ b/test/UpgradeableRegistrarController/SetReverseRegistrar.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControllerBase.t.sol"; +import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; +import {MockReverseRegistrar} from "test/mocks/MockReverseRegistrar.sol"; +import {IReverseRegistrar} from "src/L2/interface/IReverseRegistrar.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; + +contract SetReverseRegistrar is UpgradeableRegistrarControllerBase { + function test_reverts_ifCalledByNonOwner(address caller) public { + vm.assume(caller != owner); + MockReverseRegistrar newReverse = new MockReverseRegistrar(); + vm.expectRevert(Ownable.Unauthorized.selector); + vm.prank(caller); + controller.setReverseRegistrar(IReverseRegistrar(address(newReverse))); + } + + function test_setsTheReverseRegistrarAccordingly() public { + vm.expectEmit(); + MockReverseRegistrar newReverse = new MockReverseRegistrar(); + emit UpgradeableRegistrarController.ReverseRegistrarUpdated(address(newReverse)); + vm.prank(owner); + controller.setReverseRegistrar(IReverseRegistrar(address(newReverse))); + assertEq(address(controller.reverseRegistrar()), address(newReverse)); + } +} diff --git a/test/UpgradeableRegistrarController/UpgradeableRegistrarControllerBase.t.sol b/test/UpgradeableRegistrarController/UpgradeableRegistrarControllerBase.t.sol new file mode 100644 index 0000000..3c80bd0 --- /dev/null +++ b/test/UpgradeableRegistrarController/UpgradeableRegistrarControllerBase.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {BaseRegistrar} from "src/L2/BaseRegistrar.sol"; +import {ENS} from "ens-contracts/registry/ENS.sol"; +import {IPriceOracle} from "src/L2/interface/IPriceOracle.sol"; +import {IReverseRegistrar} from "src/L2/interface/IReverseRegistrar.sol"; +import {TransparentUpgradeableProxy} from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {Registry} from "src/L2/Registry.sol"; +import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; + +import {MockBaseRegistrar} from "test/mocks/MockBaseRegistrar.sol"; +import {MockDiscountValidator} from "test/mocks/MockDiscountValidator.sol"; +import {MockNameWrapper} from "test/mocks/MockNameWrapper.sol"; +import {MockPriceOracle} from "test/mocks/MockPriceOracle.sol"; +import {MockPublicResolver} from "test/mocks/MockPublicResolver.sol"; +import {MockReverseRegistrar} from "test/mocks/MockReverseRegistrar.sol"; +import {MockReverseResolver} from "test/mocks/MockReverseResolver.sol"; +import {MockRegistrarController} from "test/mocks/MockRegistrarController.sol"; +import {BASE_ETH_NODE, REVERSE_NODE} from "src/util/Constants.sol"; + +import "forge-std/console.sol"; + +contract UpgradeableRegistrarControllerBase is Test { + UpgradeableRegistrarController public controllerImpl; + UpgradeableRegistrarController public controller; + TransparentUpgradeableProxy public proxy; + + MockBaseRegistrar public base; + MockReverseRegistrar public reverse; + MockPriceOracle public prices; + Registry public registry; + MockPublicResolver public resolver; + MockRegistrarController public legacyController; + MockReverseResolver public reverseResolver; + + address owner = makeAddr("owner"); // Ownable owner on UpgradeableRegistrarController + address admin = makeAddr("admin"); // Proxy Admin on TransparentUpgradeableProxy + address user = makeAddr("user"); + address payments = makeAddr("payments"); + + bytes32 public rootNode = BASE_ETH_NODE; + string public rootName = ".base.eth"; + string public name = "test"; + string public shortName = "t"; + bytes32 public nameLabel = keccak256(bytes(name)); + bytes32 public shortNameLabel = keccak256(bytes(shortName)); + + MockDiscountValidator public validator; + bytes32 public discountKey = keccak256(bytes("default.discount")); + uint256 discountAmount = 0.1 ether; + uint256 duration = 365 days; + + uint256 deployTime = 1720000000; // July 3, 2024 + uint256 launchTime = 1720800000; // July 12, 2024 + + function setUp() public { + base = new MockBaseRegistrar(); + reverse = new MockReverseRegistrar(); + prices = new MockPriceOracle(); + registry = new Registry(owner); + resolver = new MockPublicResolver(); + validator = new MockDiscountValidator(); + legacyController = new MockRegistrarController(launchTime); + reverseResolver = new MockReverseResolver(); + + _establishNamespace(); + + bytes memory controllerInitData = abi.encodeWithSelector( + UpgradeableRegistrarController.initialize.selector, + BaseRegistrar(address(base)), + IPriceOracle(address(prices)), + IReverseRegistrar(address(reverse)), + owner, + rootNode, + rootName, + payments, + address(legacyController), + address(reverseResolver) + ); + + vm.warp(deployTime); + vm.prank(owner); + controllerImpl = new UpgradeableRegistrarController(); + proxy = new TransparentUpgradeableProxy(address(controllerImpl), admin, controllerInitData); + controller = UpgradeableRegistrarController(address(proxy)); + } + + function _establishNamespace() internal virtual {} + + function _getDefaultDiscount() internal view returns (UpgradeableRegistrarController.DiscountDetails memory) { + return UpgradeableRegistrarController.DiscountDetails({ + active: true, + discountValidator: address(validator), + key: discountKey, + discount: discountAmount + }); + } + + function _getDefaultRegisterRequest() internal view virtual returns (UpgradeableRegistrarController.RegisterRequest memory) { + return UpgradeableRegistrarController.RegisterRequest({ + name: name, + owner: user, + duration: duration, + resolver: address(resolver), + data: _getDefaultRegisterData(), + reverseRecord: true, + signatureExpiry: 0, + signature: "" + }); + } + + function _getDefaultRegisterData() internal view virtual returns (bytes[] memory data) { + data = new bytes[](1); + data[0] = bytes(name); + } +} diff --git a/test/UpgradeableRegistrarController/Valid.t.sol b/test/UpgradeableRegistrarController/Valid.t.sol new file mode 100644 index 0000000..2a34a9c --- /dev/null +++ b/test/UpgradeableRegistrarController/Valid.t.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControllerBase.t.sol"; + +contract Valid is UpgradeableRegistrarControllerBase { + function test_returnsTrue_whenValid() public view { + assertTrue(controller.valid("abc")); + assertTrue(controller.valid("abcdef")); + assertTrue(controller.valid("abcdefghijklmnop")); + } + + function test_returnsFalse_whenInvalid() public view { + assertFalse(controller.valid("")); + assertFalse(controller.valid("a")); + assertFalse(controller.valid("ab")); + } +} diff --git a/test/UpgradeableRegistrarController/WithdrawETH.t.sol b/test/UpgradeableRegistrarController/WithdrawETH.t.sol new file mode 100644 index 0000000..2a353ee --- /dev/null +++ b/test/UpgradeableRegistrarController/WithdrawETH.t.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControllerBase.t.sol"; +import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; +import {IPriceOracle} from "src/L2/interface/IPriceOracle.sol"; + +contract WithdrawETH is UpgradeableRegistrarControllerBase { + function test_alwaysSendsTheBalanceToTheOwner(address caller) public { + vm.deal(address(controller), 1 ether); + assertEq(payments.balance, 0); + vm.prank(caller); + controller.withdrawETH(); + assertEq(payments.balance, 1 ether); + } +} diff --git a/test/mocks/MockRegistrarController.sol b/test/mocks/MockRegistrarController.sol new file mode 100644 index 0000000..046f5ee --- /dev/null +++ b/test/mocks/MockRegistrarController.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +contract MockRegistrarController { + mapping(address => bool) hasRegistered; + uint256 public launchTime; + + constructor(uint256 launchTime_) { + launchTime = launchTime_; + } + + function hasRegisteredWithDiscount(address[] memory addresses) external view returns (bool) { + for(uint256 i; i < addresses.length; i++) { + if(hasRegistered[addresses[i]]) { + return true; + } + } + return false; + } + + function setHasRegisteredWithDiscount(address addr, bool status) external { + hasRegistered[addr] = status; + } +} \ No newline at end of file From 1fc8be7dfb2f9c979e309a5bec84d1e0001a0d74 Mon Sep 17 00:00:00 2001 From: Abdulla Al-Kamil Date: Thu, 31 Oct 2024 13:05:35 +0000 Subject: [PATCH 08/13] chore: forge fmt --- src/L2/UpgradeableRegistrarController.sol | 6 +++--- .../DiscountedRegister.t.sol | 8 ++++++-- .../SetDiscountDetails.t.sol | 14 ++++++++++---- .../UpgradeableRegistrarControllerBase.t.sol | 12 +++++++++--- test/mocks/MockRegistrarController.sol | 8 ++++---- 5 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/L2/UpgradeableRegistrarController.sol b/src/L2/UpgradeableRegistrarController.sol index ed28e06..c165aff 100644 --- a/src/L2/UpgradeableRegistrarController.sol +++ b/src/L2/UpgradeableRegistrarController.sol @@ -392,7 +392,7 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { /// /// @param discountKey The uuid of the discount to fetch. /// - /// @return DiscountDetails associated with the provided `discountKey`. + /// @return DiscountDetails associated with the provided `discountKey`. function discounts(bytes32 discountKey) external view returns (DiscountDetails memory) { return _getURCStorage().discounts[discountKey]; } @@ -405,14 +405,14 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { } /// @notice Fetches the price oracle from storage. - /// + /// /// @return The stored prices oracle. function prices() external view returns (IPriceOracle) { return _getURCStorage().prices; } /// @notice Fetches the Reverse Registrar from storage. - /// + /// /// @return The stored Reverse Registrar. function reverseRegistrar() external view returns (IReverseRegistrar) { return _getURCStorage().reverseRegistrar; diff --git a/test/UpgradeableRegistrarController/DiscountedRegister.t.sol b/test/UpgradeableRegistrarController/DiscountedRegister.t.sol index b16483c..2d2193e 100644 --- a/test/UpgradeableRegistrarController/DiscountedRegister.t.sol +++ b/test/UpgradeableRegistrarController/DiscountedRegister.t.sol @@ -27,7 +27,9 @@ contract DiscountedRegister is UpgradeableRegistrarControllerBase { validator.setReturnValue(false); uint256 price = controller.discountedRegisterPrice(name, duration, discountKey); - vm.expectRevert(abi.encodeWithSelector(UpgradeableRegistrarController.InvalidDiscount.selector, discountKey, "")); + vm.expectRevert( + abi.encodeWithSelector(UpgradeableRegistrarController.InvalidDiscount.selector, discountKey, "") + ); vm.prank(user); controller.discountedRegister{value: price}(_getDefaultRegisterRequest(), discountKey, ""); } @@ -136,7 +138,9 @@ contract DiscountedRegister is UpgradeableRegistrarControllerBase { vm.prank(user); controller.discountedRegister{value: price}(request, discountKey, ""); - vm.expectRevert(abi.encodeWithSelector(UpgradeableRegistrarController.AlreadyRegisteredWithDiscount.selector, user)); + vm.expectRevert( + abi.encodeWithSelector(UpgradeableRegistrarController.AlreadyRegisteredWithDiscount.selector, user) + ); request.name = "newname"; vm.prank(user); controller.discountedRegister{value: price}(request, discountKey, ""); diff --git a/test/UpgradeableRegistrarController/SetDiscountDetails.t.sol b/test/UpgradeableRegistrarController/SetDiscountDetails.t.sol index ac47a4e..11572fc 100644 --- a/test/UpgradeableRegistrarController/SetDiscountDetails.t.sol +++ b/test/UpgradeableRegistrarController/SetDiscountDetails.t.sol @@ -16,7 +16,9 @@ contract SetDiscountDetails is UpgradeableRegistrarControllerBase { function test_reverts_ifTheDiscountIsZero() public { UpgradeableRegistrarController.DiscountDetails memory noDiscount = _getDefaultDiscount(); noDiscount.discount = 0; - vm.expectRevert(abi.encodeWithSelector(UpgradeableRegistrarController.InvalidDiscountAmount.selector, discountKey)); + vm.expectRevert( + abi.encodeWithSelector(UpgradeableRegistrarController.InvalidDiscountAmount.selector, discountKey) + ); vm.prank(owner); controller.setDiscountDetails(noDiscount); } @@ -24,7 +26,9 @@ contract SetDiscountDetails is UpgradeableRegistrarControllerBase { function test_reverts_ifTheDiscounValidatorIsInvalid() public { UpgradeableRegistrarController.DiscountDetails memory noValidator = _getDefaultDiscount(); noValidator.discountValidator = address(0); - vm.expectRevert(abi.encodeWithSelector(UpgradeableRegistrarController.InvalidValidator.selector, discountKey, address(0))); + vm.expectRevert( + abi.encodeWithSelector(UpgradeableRegistrarController.InvalidValidator.selector, discountKey, address(0)) + ); vm.prank(owner); controller.setDiscountDetails(noValidator); } @@ -46,7 +50,8 @@ contract SetDiscountDetails is UpgradeableRegistrarControllerBase { vm.prank(owner); controller.setDiscountDetails(discountDetails); - UpgradeableRegistrarController.DiscountDetails[] memory activeDiscountsWithActive = controller.getActiveDiscounts(); + UpgradeableRegistrarController.DiscountDetails[] memory activeDiscountsWithActive = + controller.getActiveDiscounts(); assertEq(activeDiscountsWithActive.length, 1); assertTrue(activeDiscountsWithActive[0].active); assertEq(activeDiscountsWithActive[0].discountValidator, address(validator)); @@ -56,7 +61,8 @@ contract SetDiscountDetails is UpgradeableRegistrarControllerBase { discountDetails.active = false; vm.prank(owner); controller.setDiscountDetails(discountDetails); - UpgradeableRegistrarController.DiscountDetails[] memory activeDiscountsNoneActive = controller.getActiveDiscounts(); + UpgradeableRegistrarController.DiscountDetails[] memory activeDiscountsNoneActive = + controller.getActiveDiscounts(); assertEq(activeDiscountsNoneActive.length, 0); } } diff --git a/test/UpgradeableRegistrarController/UpgradeableRegistrarControllerBase.t.sol b/test/UpgradeableRegistrarController/UpgradeableRegistrarControllerBase.t.sol index 3c80bd0..a3288c8 100644 --- a/test/UpgradeableRegistrarController/UpgradeableRegistrarControllerBase.t.sol +++ b/test/UpgradeableRegistrarController/UpgradeableRegistrarControllerBase.t.sol @@ -6,7 +6,8 @@ import {BaseRegistrar} from "src/L2/BaseRegistrar.sol"; import {ENS} from "ens-contracts/registry/ENS.sol"; import {IPriceOracle} from "src/L2/interface/IPriceOracle.sol"; import {IReverseRegistrar} from "src/L2/interface/IReverseRegistrar.sol"; -import {TransparentUpgradeableProxy} from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {TransparentUpgradeableProxy} from + "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {Registry} from "src/L2/Registry.sol"; import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; @@ -68,7 +69,7 @@ contract UpgradeableRegistrarControllerBase is Test { _establishNamespace(); bytes memory controllerInitData = abi.encodeWithSelector( - UpgradeableRegistrarController.initialize.selector, + UpgradeableRegistrarController.initialize.selector, BaseRegistrar(address(base)), IPriceOracle(address(prices)), IReverseRegistrar(address(reverse)), @@ -98,7 +99,12 @@ contract UpgradeableRegistrarControllerBase is Test { }); } - function _getDefaultRegisterRequest() internal view virtual returns (UpgradeableRegistrarController.RegisterRequest memory) { + function _getDefaultRegisterRequest() + internal + view + virtual + returns (UpgradeableRegistrarController.RegisterRequest memory) + { return UpgradeableRegistrarController.RegisterRequest({ name: name, owner: user, diff --git a/test/mocks/MockRegistrarController.sol b/test/mocks/MockRegistrarController.sol index 046f5ee..21fb0cf 100644 --- a/test/mocks/MockRegistrarController.sol +++ b/test/mocks/MockRegistrarController.sol @@ -4,14 +4,14 @@ pragma solidity ^0.8.23; contract MockRegistrarController { mapping(address => bool) hasRegistered; uint256 public launchTime; - + constructor(uint256 launchTime_) { launchTime = launchTime_; } function hasRegisteredWithDiscount(address[] memory addresses) external view returns (bool) { - for(uint256 i; i < addresses.length; i++) { - if(hasRegistered[addresses[i]]) { + for (uint256 i; i < addresses.length; i++) { + if (hasRegistered[addresses[i]]) { return true; } } @@ -21,4 +21,4 @@ contract MockRegistrarController { function setHasRegisteredWithDiscount(address addr, bool status) external { hasRegistered[addr] = status; } -} \ No newline at end of file +} From 15ebd14498803df451f8edf8a4a6a66118cc8746 Mon Sep 17 00:00:00 2001 From: Abdulla Al-Kamil Date: Thu, 31 Oct 2024 13:06:49 +0000 Subject: [PATCH 09/13] chore: typos --- src/L2/UpgradeableRegistrarController.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/L2/UpgradeableRegistrarController.sol b/src/L2/UpgradeableRegistrarController.sol index c165aff..8ed68fd 100644 --- a/src/L2/UpgradeableRegistrarController.sol +++ b/src/L2/UpgradeableRegistrarController.sol @@ -42,7 +42,7 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { 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. + /// @dev Multicallable data bytes for setting records in the associated resolver upon registration. bytes[] data; /// @dev Bool to decide whether to set this name as the "primary" name for the `owner`. bool reverseRecord; @@ -448,7 +448,7 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { /// @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 + /// @return price The all-in 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 @@ -574,7 +574,7 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { return expires + GRACE_PERIOD; } - /// @notice Shared registartion logic for both `register()` and `discountedRegister()`. + /// @notice Shared registration 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`. @@ -636,7 +636,7 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { bytes memory signature ) internal { URCStorage storage $ = _getURCStorage(); - // vesitigial reverse resolution + // vestigial reverse resolution $.reverseRegistrar.setNameForAddr(msg.sender, owner, resolver, string.concat(name, $.rootName)); // new reverse resolver IL2ReverseResolver($.reverseResolver).setNameForAddrWithSignature(msg.sender, name, expiry, signature); From d256095404653a39316fd313da7106a9c9b879e4 Mon Sep 17 00:00:00 2001 From: Abdulla Al-Kamil Date: Thu, 31 Oct 2024 13:54:22 +0000 Subject: [PATCH 10/13] forge install: openzeppelin-contracts-upgradeable v5.1.0 --- .gitmodules | 3 +++ lib/openzeppelin-contracts-upgradeable | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/openzeppelin-contracts-upgradeable diff --git a/.gitmodules b/.gitmodules index 20e3bbb..c0dee49 100644 --- a/.gitmodules +++ b/.gitmodules @@ -34,3 +34,6 @@ [submodule "lib/eas-contracts"] path = lib/eas-contracts url = https://github.com/ethereum-attestation-service/eas-contracts +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/openzeppelin/openzeppelin-contracts-upgradeable diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..fa52531 --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit fa525310e45f91eb20a6d3baa2644be8e0adba31 From 5214bc298c3b7310c18502d3fd7b05b80d131e20 Mon Sep 17 00:00:00 2001 From: Abdulla Al-Kamil Date: Thu, 31 Oct 2024 14:18:08 +0000 Subject: [PATCH 11/13] feat: use v5.0.0 of openzeppelin-contracts-upgradeable --- foundry.toml | 14 ++++++++++++-- src/L2/UpgradeableRegistrarController.sol | 5 ++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/foundry.toml b/foundry.toml index 1e08ef1..2a2e2e4 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,8 +2,19 @@ src = "src" out = "out" libs = ["lib"] -remappings = ["@ensdomains/buffer/=lib/buffer"] +remappings = [ + "@ensdomains/buffer/=lib/buffer", + "solady/=lib/solady/src/", + "forge-std/=lib/forge-std/src/", + "ens-contracts/=lib/ens-contracts/contracts/", + "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", + "openzeppelin-contracts/=lib/openzeppelin-contracts", + "eas-contracts/=lib/eas-contracts/contracts/", + "verifications/=lib/verifications/src", + "openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/" +] fs_permissions = [{access = "read", path = "./script/premint/"}] +auto_detect_remappings = false [rpc_endpoints] sepolia="${SEPOLIA_RPC_URL}" @@ -12,4 +23,3 @@ base-sepolia="${BASE_SEPOLIA_RPC_URL}" [etherscan] sepolia={url = "https://api-sepolia.etherscan.io/api", key = "${ETHERSCAN_API_KEY}"} base-sepolia={url = "https://api-sepolia.basescan.org/api", key = "${BASE_ETHERSCAN_API_KEY}"} - diff --git a/src/L2/UpgradeableRegistrarController.sol b/src/L2/UpgradeableRegistrarController.sol index 8ed68fd..b406624 100644 --- a/src/L2/UpgradeableRegistrarController.sol +++ b/src/L2/UpgradeableRegistrarController.sol @@ -3,7 +3,7 @@ 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 {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/access/OwnableUpgradeable.sol"; import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import {StringUtils} from "ens-contracts/ethregistrar/StringUtils.sol"; @@ -288,8 +288,7 @@ contract UpgradeableRegistrarController is OwnableUpgradeable { address legacyRegistrarController_, address reverseResolver_ ) public initializer onlyInitializing { - __Ownable_init(); - transferOwnership(owner_); + __Ownable_init(owner_); URCStorage storage $ = _getURCStorage(); $.base = base_; From 34a313a47d0b8f3ba052deb1ef3d5f95e0ff39cb Mon Sep 17 00:00:00 2001 From: Abdulla Al-Kamil Date: Fri, 1 Nov 2024 11:17:39 +0000 Subject: [PATCH 12/13] feat: update tests to use OpenZeppelin Upgradeable --- .../UpgradeableRegistrarController/RecoverFunds.t.sol | 11 +++++++---- .../SetDiscountDetails.t.sol | 6 +++--- .../SetPaymentReceiver.t.sol | 6 +++--- .../SetPriceOracle.t.sol | 6 +++--- .../SetReverseRegistrar.t.sol | 6 +++--- .../UpgradeableRegistrarControllerBase.t.sol | 8 ++++++++ test/UpgradeableRegistrarController/WithdrawETH.t.sol | 5 ++++- 7 files changed, 31 insertions(+), 17 deletions(-) diff --git a/test/UpgradeableRegistrarController/RecoverFunds.t.sol b/test/UpgradeableRegistrarController/RecoverFunds.t.sol index c7517b6..9d68c6c 100644 --- a/test/UpgradeableRegistrarController/RecoverFunds.t.sol +++ b/test/UpgradeableRegistrarController/RecoverFunds.t.sol @@ -4,16 +4,19 @@ pragma solidity ^0.8.23; import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControllerBase.t.sol"; import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; import {IPriceOracle} from "src/L2/interface/IPriceOracle.sol"; -import {Ownable} from "solady/auth/Ownable.sol"; +import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/access/OwnableUpgradeable.sol"; import {MockUSDC} from "test/mocks/MockUSDC.sol"; contract RecoverFunds is UpgradeableRegistrarControllerBase { MockUSDC public usdc; - function test_reverts_ifCalledByNonOwner(address caller, uint256 amount) public { - vm.assume(caller != owner); + function test_reverts_ifCalledByNonOwner(address caller, uint256 amount) + public + whenNotProxyAdmin(caller, address(controller)) + { + vm.assume(caller != owner); // Ownable owner vm.assume(amount > 0 && amount < type(uint128).max); - vm.expectRevert(Ownable.Unauthorized.selector); + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, caller)); vm.prank(caller); controller.recoverFunds(address(usdc), caller, amount); } diff --git a/test/UpgradeableRegistrarController/SetDiscountDetails.t.sol b/test/UpgradeableRegistrarController/SetDiscountDetails.t.sol index 11572fc..f8edf07 100644 --- a/test/UpgradeableRegistrarController/SetDiscountDetails.t.sol +++ b/test/UpgradeableRegistrarController/SetDiscountDetails.t.sol @@ -3,12 +3,12 @@ pragma solidity ^0.8.23; import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControllerBase.t.sol"; import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; -import {Ownable} from "solady/auth/Ownable.sol"; +import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/access/OwnableUpgradeable.sol"; contract SetDiscountDetails is UpgradeableRegistrarControllerBase { - function test_reverts_ifCalledByNonOwner(address caller) public { + function test_reverts_ifCalledByNonOwner(address caller) public whenNotProxyAdmin(caller, address(controller)) { vm.assume(caller != owner); - vm.expectRevert(Ownable.Unauthorized.selector); + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, caller)); vm.prank(caller); controller.setDiscountDetails(_getDefaultDiscount()); } diff --git a/test/UpgradeableRegistrarController/SetPaymentReceiver.t.sol b/test/UpgradeableRegistrarController/SetPaymentReceiver.t.sol index 32f6010..c68803a 100644 --- a/test/UpgradeableRegistrarController/SetPaymentReceiver.t.sol +++ b/test/UpgradeableRegistrarController/SetPaymentReceiver.t.sol @@ -3,12 +3,12 @@ pragma solidity ^0.8.23; import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControllerBase.t.sol"; import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; -import {Ownable} from "solady/auth/Ownable.sol"; +import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/access/OwnableUpgradeable.sol"; contract SetPaymentReceiver is UpgradeableRegistrarControllerBase { - function test_reverts_ifCalledByNonOwner(address caller) public { + function test_reverts_ifCalledByNonOwner(address caller) public whenNotProxyAdmin(caller, address(controller)) { vm.assume(caller != owner && caller != address(0)); - vm.expectRevert(Ownable.Unauthorized.selector); + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, caller)); vm.prank(caller); controller.setPaymentReceiver(caller); } diff --git a/test/UpgradeableRegistrarController/SetPriceOracle.t.sol b/test/UpgradeableRegistrarController/SetPriceOracle.t.sol index b8bc395..0b549e4 100644 --- a/test/UpgradeableRegistrarController/SetPriceOracle.t.sol +++ b/test/UpgradeableRegistrarController/SetPriceOracle.t.sol @@ -5,13 +5,13 @@ import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControll import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; import {MockPriceOracle} from "test/mocks/MockPriceOracle.sol"; import {IPriceOracle} from "src/L2/interface/IPriceOracle.sol"; -import {Ownable} from "solady/auth/Ownable.sol"; +import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/access/OwnableUpgradeable.sol"; contract SetPriceOracle is UpgradeableRegistrarControllerBase { - function test_reverts_ifCalledByNonOwner(address caller) public { + function test_reverts_ifCalledByNonOwner(address caller) public whenNotProxyAdmin(caller, address(controller)) { vm.assume(caller != owner); MockPriceOracle newPrices = new MockPriceOracle(); - vm.expectRevert(Ownable.Unauthorized.selector); + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, caller)); vm.prank(caller); controller.setPriceOracle(IPriceOracle(address(newPrices))); } diff --git a/test/UpgradeableRegistrarController/SetReverseRegistrar.t.sol b/test/UpgradeableRegistrarController/SetReverseRegistrar.t.sol index 399ca6f..11821e1 100644 --- a/test/UpgradeableRegistrarController/SetReverseRegistrar.t.sol +++ b/test/UpgradeableRegistrarController/SetReverseRegistrar.t.sol @@ -5,13 +5,13 @@ import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControll import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; import {MockReverseRegistrar} from "test/mocks/MockReverseRegistrar.sol"; import {IReverseRegistrar} from "src/L2/interface/IReverseRegistrar.sol"; -import {Ownable} from "solady/auth/Ownable.sol"; +import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/access/OwnableUpgradeable.sol"; contract SetReverseRegistrar is UpgradeableRegistrarControllerBase { - function test_reverts_ifCalledByNonOwner(address caller) public { + function test_reverts_ifCalledByNonOwner(address caller) public whenNotProxyAdmin(caller, address(controller)) { vm.assume(caller != owner); MockReverseRegistrar newReverse = new MockReverseRegistrar(); - vm.expectRevert(Ownable.Unauthorized.selector); + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, caller)); vm.prank(caller); controller.setReverseRegistrar(IReverseRegistrar(address(newReverse))); } diff --git a/test/UpgradeableRegistrarController/UpgradeableRegistrarControllerBase.t.sol b/test/UpgradeableRegistrarController/UpgradeableRegistrarControllerBase.t.sol index a3288c8..77d0d77 100644 --- a/test/UpgradeableRegistrarController/UpgradeableRegistrarControllerBase.t.sol +++ b/test/UpgradeableRegistrarController/UpgradeableRegistrarControllerBase.t.sol @@ -20,6 +20,7 @@ import {MockReverseRegistrar} from "test/mocks/MockReverseRegistrar.sol"; import {MockReverseResolver} from "test/mocks/MockReverseResolver.sol"; import {MockRegistrarController} from "test/mocks/MockRegistrarController.sol"; import {BASE_ETH_NODE, REVERSE_NODE} from "src/util/Constants.sol"; +import {ERC1967Utils} from "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol"; import "forge-std/console.sol"; @@ -121,4 +122,11 @@ contract UpgradeableRegistrarControllerBase is Test { data = new bytes[](1); data[0] = bytes(name); } + + modifier whenNotProxyAdmin(address caller, address proxyContract) { + // The _admin on the Proxy is not exposed externally, although can be loaded from the ERC1967 admin slot + address proxyAdmin = address(uint160(uint256(vm.load(address(proxyContract), ERC1967Utils.ADMIN_SLOT)))); + vm.assume(caller != proxyAdmin); // proxy admin on transparent upgradeable proxy + _; + } } diff --git a/test/UpgradeableRegistrarController/WithdrawETH.t.sol b/test/UpgradeableRegistrarController/WithdrawETH.t.sol index 2a353ee..074306f 100644 --- a/test/UpgradeableRegistrarController/WithdrawETH.t.sol +++ b/test/UpgradeableRegistrarController/WithdrawETH.t.sol @@ -6,7 +6,10 @@ import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarControl import {IPriceOracle} from "src/L2/interface/IPriceOracle.sol"; contract WithdrawETH is UpgradeableRegistrarControllerBase { - function test_alwaysSendsTheBalanceToTheOwner(address caller) public { + function test_alwaysSendsTheBalanceToTheOwner(address caller) + public + whenNotProxyAdmin(caller, address(controller)) + { vm.deal(address(controller), 1 ether); assertEq(payments.balance, 0); vm.prank(caller); From 78e3f281c107e62b76b425e87323f4c094ea382f Mon Sep 17 00:00:00 2001 From: Abdulla Al-Kamil Date: Fri, 1 Nov 2024 16:44:34 +0000 Subject: [PATCH 13/13] chore: fix typo --- test/RegistrarController/RegisterPrice.t.sol | 2 +- test/RegistrarController/RentPrice.t.sol | 2 +- test/UpgradeableRegistrarController/RegisterPrice.t.sol | 2 +- test/UpgradeableRegistrarController/RentPrice.t.sol | 2 +- test/mocks/MockPriceOracle.sol | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/RegistrarController/RegisterPrice.t.sol b/test/RegistrarController/RegisterPrice.t.sol index 096a092..b2b6296 100644 --- a/test/RegistrarController/RegisterPrice.t.sol +++ b/test/RegistrarController/RegisterPrice.t.sol @@ -7,7 +7,7 @@ import {IPriceOracle} from "src/L2/interface/IPriceOracle.sol"; contract RegisterPrice is RegistrarControllerBase { function test_returnsRegisterPrice_fromPricingOracle() public view { uint256 retPrice = controller.registerPrice(name, 0); - assertEq(retPrice, prices.DEFAULT_BASE_WEI() + prices.DEFAULT_PERMIUM_WEI()); + assertEq(retPrice, prices.DEFAULT_BASE_WEI() + prices.DEFAULT_PREMIUM_WEI()); } function test_fuzz_returnsRegisterPrice_fromPricingOracle(uint256 fuzzBase, uint256 fuzzPremium) public { diff --git a/test/RegistrarController/RentPrice.t.sol b/test/RegistrarController/RentPrice.t.sol index 77a18fa..1e5ad64 100644 --- a/test/RegistrarController/RentPrice.t.sol +++ b/test/RegistrarController/RentPrice.t.sol @@ -8,7 +8,7 @@ contract RentPrice is RegistrarControllerBase { function test_returnsPrice_fromPricingOracle() public view { IPriceOracle.Price memory retPrices = controller.rentPrice(name, 0); assertEq(retPrices.base, prices.DEFAULT_BASE_WEI()); - assertEq(retPrices.premium, prices.DEFAULT_PERMIUM_WEI()); + assertEq(retPrices.premium, prices.DEFAULT_PREMIUM_WEI()); } function test_returnsPremium_ifTimeIsNearLaunchTime() public { diff --git a/test/UpgradeableRegistrarController/RegisterPrice.t.sol b/test/UpgradeableRegistrarController/RegisterPrice.t.sol index 5eb2e9e..4e6709b 100644 --- a/test/UpgradeableRegistrarController/RegisterPrice.t.sol +++ b/test/UpgradeableRegistrarController/RegisterPrice.t.sol @@ -7,7 +7,7 @@ import {IPriceOracle} from "src/L2/interface/IPriceOracle.sol"; contract RegisterPrice is UpgradeableRegistrarControllerBase { function test_returnsRegisterPrice_fromPricingOracle() public view { uint256 retPrice = controller.registerPrice(name, 0); - assertEq(retPrice, prices.DEFAULT_BASE_WEI() + prices.DEFAULT_PERMIUM_WEI()); + assertEq(retPrice, prices.DEFAULT_BASE_WEI() + prices.DEFAULT_PREMIUM_WEI()); } function test_fuzz_returnsRegisterPrice_fromPricingOracle(uint256 fuzzBase, uint256 fuzzPremium) public { diff --git a/test/UpgradeableRegistrarController/RentPrice.t.sol b/test/UpgradeableRegistrarController/RentPrice.t.sol index c90a37b..99d33dc 100644 --- a/test/UpgradeableRegistrarController/RentPrice.t.sol +++ b/test/UpgradeableRegistrarController/RentPrice.t.sol @@ -8,7 +8,7 @@ contract RentPrice is UpgradeableRegistrarControllerBase { function test_returnsPrice_fromPricingOracle() public view { IPriceOracle.Price memory retPrices = controller.rentPrice(name, 0); assertEq(retPrices.base, prices.DEFAULT_BASE_WEI()); - assertEq(retPrices.premium, prices.DEFAULT_PERMIUM_WEI()); + assertEq(retPrices.premium, prices.DEFAULT_PREMIUM_WEI()); } function test_returnsPremium_ifTimeIsNearLaunchTime() public { diff --git a/test/mocks/MockPriceOracle.sol b/test/mocks/MockPriceOracle.sol index 54be2db..d33fe92 100644 --- a/test/mocks/MockPriceOracle.sol +++ b/test/mocks/MockPriceOracle.sol @@ -6,10 +6,10 @@ import {GRACE_PERIOD} from "src/util/Constants.sol"; contract MockPriceOracle is IPriceOracle { uint256 public constant DEFAULT_BASE_WEI = 0.1 ether; - uint256 public constant DEFAULT_PERMIUM_WEI = 0; + uint256 public constant DEFAULT_PREMIUM_WEI = 0; uint256 public constant DEFAULT_INCLUDED_PREMIUM = 0.2 ether; - IPriceOracle.Price public defaultPrice = IPriceOracle.Price({base: DEFAULT_BASE_WEI, premium: DEFAULT_PERMIUM_WEI}); + IPriceOracle.Price public defaultPrice = IPriceOracle.Price({base: DEFAULT_BASE_WEI, premium: DEFAULT_PREMIUM_WEI}); mapping(string => IPriceOracle.Price) prices;