diff --git a/.gitmodules b/.gitmodules index 20e3bbb2..c0dee490 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/foundry.toml b/foundry.toml index 1e08ef17..2a2e2e45 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/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 00000000..fa525310 --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit fa525310e45f91eb20a6d3baa2644be8e0adba31 diff --git a/src/L2/ReverseRegistrarShim.sol b/src/L2/ReverseRegistrarShim.sol new file mode 100644 index 00000000..aac71511 --- /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 new file mode 100644 index 00000000..b406624b --- /dev/null +++ b/src/L2/UpgradeableRegistrarController.sol @@ -0,0 +1,675 @@ +// 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-contracts-upgradeable/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 {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 +/// +/// @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 registration. + 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. + 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; + } + + 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 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. + 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 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 */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @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_, + address reverseResolver_ + ) public initializer onlyInitializing { + __Ownable_init(owner_); + + URCStorage storage $ = _getURCStorage(); + $.base = base_; + $.prices = prices_; + $.reverseRegistrar = reverseRegistrar_; + $.rootNode = rootNode_; + $.rootName = rootName_; + $.paymentReceiver = paymentReceiver_; + $.legacyRegistrarController = legacyRegistrarController_; + $.reverseResolver = reverseResolver_; + $.launchTime = RegistrarController(legacyRegistrarController_).launchTime(); + } + + /// @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 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]] + || RegistrarController($.legacyRegistrarController).hasRegisteredWithDiscount(addresses) + ) { + 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)); + 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`. + /// + /// @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-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 + 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 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`. + /// Emits `NameRegistered` upon successful registration. + /// + /// @param request The `RegisterRequest` struct containing the details for the registration. + function _register(RegisterRequest calldata request) internal { + uint256 expires = _getURCStorage().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, request.signatureExpiry, request.signature); + } + + 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 { + bytes32 nodehash = keccak256(abi.encodePacked(_getURCStorage().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, + uint256 expiry, + bytes memory signature + ) internal { + URCStorage storage $ = _getURCStorage(); + // vestigial 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. + /// + /// @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 { + (bool sent,) = payable(_getURCStorage().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 + } + } +} diff --git a/src/L2/interface/IL2ReverseResolver.sol b/src/L2/interface/IL2ReverseResolver.sol new file mode 100644 index 00000000..f08fa5d9 --- /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); +} diff --git a/test/RegistrarController/RegisterPrice.t.sol b/test/RegistrarController/RegisterPrice.t.sol index 096a092b..b2b6296d 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 77a18fa6..1e5ad642 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/ReverseRegistrarShim/ReverseRegistrarShimBase.t.sol b/test/ReverseRegistrarShim/ReverseRegistrarShimBase.t.sol new file mode 100644 index 00000000..f990099d --- /dev/null +++ b/test/ReverseRegistrarShim/ReverseRegistrarShimBase.t.sol @@ -0,0 +1,34 @@ +//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 00000000..751cd0e4 --- /dev/null +++ b/test/ReverseRegistrarShim/SetNameForAddrWithSignature.t.sol @@ -0,0 +1,28 @@ +//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); + } +} diff --git a/test/UpgradeableRegistrarController/Available.t.sol b/test/UpgradeableRegistrarController/Available.t.sol new file mode 100644 index 00000000..a2bbfbd8 --- /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 00000000..2d2193e1 --- /dev/null +++ b/test/UpgradeableRegistrarController/DiscountedRegister.t.sol @@ -0,0 +1,148 @@ +// 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 00000000..b149922c --- /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 00000000..9d68c6c4 --- /dev/null +++ b/test/UpgradeableRegistrarController/RecoverFunds.t.sol @@ -0,0 +1,38 @@ +// 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 {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 + whenNotProxyAdmin(caller, address(controller)) + { + vm.assume(caller != owner); // Ownable owner + vm.assume(amount > 0 && amount < type(uint128).max); + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, caller)); + 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 00000000..607fdcc2 --- /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 00000000..4e6709b8 --- /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_PREMIUM_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 00000000..846b1677 --- /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 00000000..99d33dce --- /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_PREMIUM_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 00000000..f8edf073 --- /dev/null +++ b/test/UpgradeableRegistrarController/SetDiscountDetails.t.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UpgradeableRegistrarControllerBase} from "./UpgradeableRegistrarControllerBase.t.sol"; +import {UpgradeableRegistrarController} from "src/L2/UpgradeableRegistrarController.sol"; +import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract SetDiscountDetails is UpgradeableRegistrarControllerBase { + function test_reverts_ifCalledByNonOwner(address caller) public whenNotProxyAdmin(caller, address(controller)) { + vm.assume(caller != owner); + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, caller)); + 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 00000000..c68803a3 --- /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 {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract SetPaymentReceiver is UpgradeableRegistrarControllerBase { + function test_reverts_ifCalledByNonOwner(address caller) public whenNotProxyAdmin(caller, address(controller)) { + vm.assume(caller != owner && caller != address(0)); + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, caller)); + 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 00000000..0b549e4c --- /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 {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract SetPriceOracle is UpgradeableRegistrarControllerBase { + function test_reverts_ifCalledByNonOwner(address caller) public whenNotProxyAdmin(caller, address(controller)) { + vm.assume(caller != owner); + MockPriceOracle newPrices = new MockPriceOracle(); + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, caller)); + 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 00000000..11821e1f --- /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 {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract SetReverseRegistrar is UpgradeableRegistrarControllerBase { + function test_reverts_ifCalledByNonOwner(address caller) public whenNotProxyAdmin(caller, address(controller)) { + vm.assume(caller != owner); + MockReverseRegistrar newReverse = new MockReverseRegistrar(); + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, caller)); + 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 00000000..77d0d77c --- /dev/null +++ b/test/UpgradeableRegistrarController/UpgradeableRegistrarControllerBase.t.sol @@ -0,0 +1,132 @@ +// 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 {ERC1967Utils} from "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.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); + } + + 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/Valid.t.sol b/test/UpgradeableRegistrarController/Valid.t.sol new file mode 100644 index 00000000..2a34a9cc --- /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 00000000..074306fa --- /dev/null +++ b/test/UpgradeableRegistrarController/WithdrawETH.t.sol @@ -0,0 +1,19 @@ +// 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 + whenNotProxyAdmin(caller, address(controller)) + { + 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/MockPriceOracle.sol b/test/mocks/MockPriceOracle.sol index 54be2db1..d33fe928 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; diff --git a/test/mocks/MockRegistrarController.sol b/test/mocks/MockRegistrarController.sol new file mode 100644 index 00000000..21fb0cfb --- /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; + } +} diff --git a/test/mocks/MockReverseResolver.sol b/test/mocks/MockReverseResolver.sol new file mode 100644 index 00000000..a233f0af --- /dev/null +++ b/test/mocks/MockReverseResolver.sol @@ -0,0 +1,12 @@ +//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); + } +}