diff --git a/src/L1/L1Resolver.sol b/src/L1/L1Resolver.sol index 439fb303..b69ec8e0 100644 --- a/src/L1/L1Resolver.sol +++ b/src/L1/L1Resolver.sol @@ -5,26 +5,76 @@ import {IExtendedResolver} from "ens-contracts/resolvers/profiles/IExtendedResol import {ERC165} from "lib/openzeppelin-contracts/contracts/utils/introspection/ERC165.sol"; import {Ownable} from "solady/auth/Ownable.sol"; -import {SignatureVerifier} from "src/lib/SignatureVerifier.sol"; import {BASE_ETH_NAME} from "src/util/Constants.sol"; +import {SignatureVerifier} from "src/lib/SignatureVerifier.sol"; -/** - * Implements an ENS resolver that directs all queries to a CCIP read gateway. - * Callers must implement EIP 3668 and ENSIP 10. - */ +/// @title L1 Resolver +/// +/// @notice Resolver for the `base.eth` domain on Ethereum mainnet. +/// It serves two primary functions: +/// 1. Resolve base.eth using existing records stored on the `rootResolver` via the `fallback` passthrough +/// 2. Initiate and verify wildcard resolution requests, compliant with CCIP-Read aka. ERC-3668 +/// https://eips.ethereum.org/EIPS/eip-3668 +/// +/// Inspired by ENS's `OffchainResolver`: +/// https://github.com/ensdomains/offchain-resolver/blob/main/packages/contracts/contracts/OffchainResolver.sol +/// +/// @author Coinbase (https://github.com/base-org/usernames) contract L1Resolver is IExtendedResolver, ERC165, Ownable { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STORAGE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + /// @notice The url endpoint for the CCIP gateway service. string public url; + /// @notice Storage of approved signers. mapping(address => bool) public signers; + /// @notice address of the rootResolver for `base.eth`. address public rootResolver; + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Thrown when an invalid signer is returned from ECDSA recovery. error InvalidSigner(); + + /// @notice Thrown when initiaitng a CCIP-read, per ERC-3668 error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData); + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* EVENTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Emitted when new signers were added to the approved `signers` mapping. event NewSigners(address[] signers); + + /// @notice Emitted when a new gateway url was stored for `url`. + /// + /// @param newUrl the new url being stored. event NewUrl(string newUrl); + + /// @notice Emitted when a new root resolver is set as the `rootResolver`. + /// + /// @param resolver The address of the new root resolver. event NewRootResolver(address resolver); + + ///@notice Emitted when a signer has been removed from the approved `signers` mapping. + /// + /// @param signer The signer that was removed from the mapping. event RemovedSigner(address signer); + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* IMPLEMENTATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Resolver constructor + /// + /// @dev Emits `NewSigners(signers_)` after setting the mapping for each signer in the `signers_` arg. + /// + /// @param url_ The gateway url stored as `url`. + /// @param signers_ The approved signers array, each stored as approved in the `signers` mapping. + /// @param owner_ The permissioned address initialized as the `owner` in the `Ownable` context. + /// @param rootResolver_ The address stored as the `rootResolver`. constructor(string memory url_, address[] memory signers_, address owner_, address rootResolver_) { url = url_; _initializeOwner(owner_); @@ -36,11 +86,21 @@ contract L1Resolver is IExtendedResolver, ERC165, Ownable { emit NewSigners(signers_); } + /// @notice Permissioned method letting the owner set the gateway url. + /// + /// @dev Emits `NewUrl(url_)` after storing the new url as `url`. + /// + /// @param url_ The gateway url stored as `url`. function setUrl(string calldata url_) external onlyOwner { url = url_; emit NewUrl(url_); } + /// @notice Permissioned method letting the owner add approved signers. + /// + /// @dev Emits `NewSigners(signers_)` after setting the mapping for each signer in the `signers_` arg. + /// + /// @param _signers Array of signers to set as approved signers in the `signers` mapping. function addSigners(address[] calldata _signers) external onlyOwner { for (uint256 i; i < _signers.length; i++) { signers[_signers[i]] = true; @@ -48,6 +108,11 @@ contract L1Resolver is IExtendedResolver, ERC165, Ownable { emit NewSigners(_signers); } + /// @notice Permissioned method letting the owner remove a signer from the approved `signers` mapping. + /// + /// @dev Emits `RemovedSigner(signer)` after setting the signer to false in the `signers` mapping. + /// + /// @param signer The signer to remove from the `signers` mapping. function removeSigner(address signer) external onlyOwner { if (signers[signer]) { delete signers[signer]; @@ -55,11 +120,24 @@ contract L1Resolver is IExtendedResolver, ERC165, Ownable { } } + /// @notice Permissioned method letting the owner set the address of the root resolver. + /// + /// @dev Emits `NewRootResolver(rootResolver_)` after setting the `rootResolver` address. + /// + /// @param rootResolver_ Address of the new `rootResolver` function setRootResolver(address rootResolver_) external onlyOwner { rootResolver = rootResolver_; emit NewRootResolver(rootResolver_); } + /// @notice Hook into the SignatureVerifier lib `makeSignatureHash` method + /// + /// @param target Address of the verifier target. + /// @param expires Expiry of the signature. + /// @param request Arbitrary bytes for the initiated request. + /// @param result Arbitrary bytes for the response to the request. + /// + /// @return The resulting signature hash. function makeSignatureHash(address target, uint64 expires, bytes memory request, bytes memory result) external pure @@ -68,38 +146,55 @@ contract L1Resolver is IExtendedResolver, ERC165, Ownable { return SignatureVerifier.makeSignatureHash(target, expires, request, result); } - /** - * Resolves a name, as specified by ENSIP 10. - * @param name The DNS-encoded name to resolve. - * @param data The ABI encoded data for the underlying resolution function (Eg, addr(bytes32), text(bytes32,string), etc). - * @return The return data, ABI encoded identically to the underlying function. - */ + /// @notice Resolves a name, as specified by ENSIP-10. + /// + /// @dev If the resolution request targets the `BASE_ETH_NAME` == base.eth, this method calls `rootResolver.resolve()` + /// Otherwise, the resolution target is implicitly a wildcard resolution request, i.e jesse.base.eth. In this case, + /// we revert with `OffchainLookup` according to ENSIP-10. + /// ENSIP-10 describes the ENS-specific mechanism to enable CCIP Reads for offchain resolution. + /// See: https://docs.ens.domains/ensip/10 + /// + /// @param name The DNS-encoded name to resolve. + /// @param data The ABI encoded data for the underlying resolution function (Eg, addr(bytes32), text(bytes32,string), etc). + /// + /// @return The return data, ABI encoded identically to the underlying function. function resolve(bytes calldata name, bytes calldata data) external view override returns (bytes memory) { - // Resolution for root name "base.eth" should query the `rootResolver` - // All other requests will be for "*.base.eth" names and should follow the CCIP flow by reverting with OffchainLookup + // Check for base.eth resolution, and resolve return early if so if (keccak256(BASE_ETH_NAME) == keccak256(name)) { return IExtendedResolver(rootResolver).resolve(name, data); } - bytes memory callData = abi.encodeWithSelector(L1Resolver.resolve.selector, name, data); + bytes memory callData = abi.encodeWithSelector(IExtendedResolver.resolve.selector, name, data); string[] memory urls = new string[](1); urls[0] = url; revert OffchainLookup(address(this), urls, callData, L1Resolver.resolveWithProof.selector, callData); } - /** - * Callback used by CCIP read compatible clients to verify and parse the response. - */ + /// @notice Callback used by CCIP read compatible clients to verify and parse the response. + /// + /// @dev The response data must be encoded per the following format: + /// response = abi.encode(bytes memory result, uint64 expires, bytes memory sig), where: + /// `result` is the resolver repsonse to the resolution request. + /// `expires` is the signature expiry. + /// `sig` is the signature data used for validating that the gateway signed the response. + /// Per ENSIP-10, the `extraData` arg must match exectly the `extraData` field from the `OffchainLookup` which initiated + /// the CCIP read. + /// Reverts with `InvalidSigner` if the recovered address is not in the `singers` mapping. + /// + /// @param response The response bytes that the client received from the gateway. + /// @param extraData The additional bytes of information from the `OffchainLookup` `extraData` arg. + /// + /// @return The bytes of the reponse from the CCIP read. function resolveWithProof(bytes calldata response, bytes calldata extraData) external view returns (bytes memory) { (address signer, bytes memory result) = SignatureVerifier.verify(extraData, response); if (!signers[signer]) revert InvalidSigner(); return result; } - /// @notice ERC165 compliant signal for interface support + /// @notice ERC165 compliant signal for interface support. /// /// @dev Checks interface support for this contract OR ERC165 OR rootResolver - /// https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + /// https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] /// /// @param interfaceID the ERC165 iface id being checked for compliance /// @@ -109,7 +204,10 @@ contract L1Resolver is IExtendedResolver, ERC165, Ownable { || ERC165(rootResolver).supportsInterface(interfaceID); } - // Handler for arbitrary resolver calls + /// @notice Generic handler for requests to the `rootResolver` + /// + /// @dev Inspired by the passthrough logic of proxy contracts, but leveraging `call` instead of `delegatecall` + /// See: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/dc625992575ecb3089acc35f5475bedfcb7e6be3/contracts/proxy/Proxy.sol#L22-L45 fallback() external { address RESOLVER = rootResolver; assembly { @@ -126,7 +224,7 @@ contract L1Resolver is IExtendedResolver, ERC165, Ownable { returndatacopy(0, 0, returndatasize()) switch result - // delegatecall returns 0 on error. + // call returns 0 on error. case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } diff --git a/src/L2/BaseRegistrar.sol b/src/L2/BaseRegistrar.sol index 44bf3b6c..508e8679 100644 --- a/src/L2/BaseRegistrar.sol +++ b/src/L2/BaseRegistrar.sol @@ -22,19 +22,27 @@ contract BaseRegistrar is ERC721, Ownable { /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* STORAGE */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - // A map of expiry times + + /// @notice A map of expiry times to name ids. mapping(uint256 id => uint256 expiry) expiries; - // The ENS registry + + /// @notice The ENS registry. ENS public ens; - // The namehash of the TLD this registrar owns (eg, .eth) + + /// @notice The namehash of the TLD this registrar owns (eg, base.eth). bytes32 public baseNode; - // A map of addresses that are authorised to register and renew names. + + /// @notice A map of addresses that are authorised to register and renew names. mapping(address controller => bool isApproved) public controllers; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* CONSTANTS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice InterfaceId for the meta `supportsInterface` method bytes4 private constant INTERFACE_META_ID = bytes4(keccak256("supportsInterface(bytes4)")); + + /// @notice InterfaceId for IERC721 bytes4 private constant ERC721_ID = bytes4( keccak256("balanceOf(address)") ^ keccak256("ownerOf(uint256)") ^ keccak256("approve(address,uint256)") ^ keccak256("getApproved(uint256)") ^ keccak256("setApprovalForAll(address,bool)") @@ -42,26 +50,75 @@ contract BaseRegistrar is ERC721, Ownable { ^ keccak256("safeTransferFrom(address,address,uint256)") ^ keccak256("safeTransferFrom(address,address,uint256,bytes)") ); + + /// @notice InterfaceId for the Reclaim interface bytes4 private constant RECLAIM_ID = bytes4(keccak256("reclaim(uint256,address)")); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* ERRORS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Thrown when the name has expired. + /// + /// @param tokenId The id of the token that expired. error Expired(uint256 tokenId); + + /// @notice Thrown when called by an unauthorized owner. + /// + /// @param tokenId The id that was being called against. + /// @param sender The unauthorized sender. error NotApprovedOwner(uint256 tokenId, address sender); + + /// @notice Thrown when the name is not available for registration. + /// + /// @param tokenId The id of the name that is not available. error NotAvailable(uint256 tokenId); + + /// @notice Thrown when the name is not registered or in its Grace Period. + /// + /// @param tokenId The id of the token that is not registered or in Grace Period. error NotRegisteredOrInGrace(uint256 tokenId); + + /// @notice Thrown when msg.sender is not an approved Controller. error OnlyController(); + + /// @notice Thrown when this contract does not own the `baseNode`. error RegistrarNotLive(); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* EVENTS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Emitted when a Controller is added to the approved `controllers` mapping. + /// + /// @param controller The address of the approved controller. event ControllerAdded(address indexed controller); + + /// @notice Emitted when a Controller is removed from the approved `controllers` mapping. + /// + /// @param controller The address of the removed controller. event ControllerRemoved(address indexed controller); - event NameMigrated(uint256 indexed id, address indexed owner, uint256 expires); + + /// @notice Emitted when a name is registered. + /// + /// @param id The id of the registered name. + /// @param owner The owner of the registered name. + /// @param expires The expiry of the new ownership record. event NameRegistered(uint256 indexed id, address indexed owner, uint256 expires); + + /// @notice Emitted when a name is renewed. + /// + /// @param id The id of the renewed name. + /// @param expires The new expiry for the name. event NameRenewed(uint256 indexed id, uint256 expires); + + /// @notice Emitted when a name is registered with ENS Records. + /// + /// @param id The id of the newly registered name. + /// @param owner The owner of the registered name. + /// @param expires The expiry of the new ownership record. + /// @param resolver The address of the resolver for the name. + /// @param ttl The time-to-live for the name. event NameRegisteredWithRecord( uint256 indexed id, address indexed owner, uint256 expires, address resolver, uint64 ttl ); @@ -69,16 +126,22 @@ contract BaseRegistrar is ERC721, Ownable { /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* MODIFIERS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Decorator for determining if the contract is actively managing registrations for its `baseNode`. modifier live() { if (ens.owner(baseNode) != address(this)) revert RegistrarNotLive(); _; } + /// @notice Decorator for restricting methods to only approved Controller callers. modifier onlyController() { if (!controllers[msg.sender]) revert OnlyController(); _; } + /// @notice Decorator for determining if a name is available. + /// + /// @param id The id being checked for availability. modifier onlyAvailable(uint256 id) { if (!isAvailable(id)) revert NotAvailable(id); _; @@ -87,52 +150,87 @@ contract BaseRegistrar is ERC721, Ownable { /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* IMPLEMENTATION */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice BaseRegistrar constructor used to initialize the configuration of the implementation. + /// + /// @param ens_ The Registry contract. + /// @param owner_ The permissioned address initialized as the `owner` in the `Ownable` context. + /// @param baseNode_ The node that this contract manages registrations for. constructor(ENS ens_, address owner_, bytes32 baseNode_) { _initializeOwner(owner_); ens = ens_; baseNode = baseNode_; } - // Authorises a controller, who can register and renew domains. + /// @notice Authorises a controller, who can register and renew domains. + /// + /// @dev Emits `ControllerAdded(controller)` after adding the `controller` to the `controllers` mapping. + /// + /// @param controller The address of the new controller. function addController(address controller) external onlyOwner { controllers[controller] = true; emit ControllerAdded(controller); } - // Revoke controller permission for an address. + /// @notice Revoke controller permission for an address. + /// + /// @dev Emits `ControllerRemoved(controller)` after removing the `controller` from the `controllers` mapping. + /// + /// @param controller The address of the controller to remove. function removeController(address controller) external onlyOwner { controllers[controller] = false; emit ControllerRemoved(controller); } - // Set the resolver for the TLD this registrar manages. + /// @notice Set the resolver for the node this registrar manages. + /// + /// @param resolver The address of the new resolver contract. function setResolver(address resolver) external onlyOwner { ens.setResolver(baseNode, resolver); } - // Returns the expiration timestamp of the specified id. + /// @notice Returns the expiration timestamp of the specified id. + /// + /// @param id The id of the name being checked. function nameExpires(uint256 id) external view returns (uint256) { return expiries[id]; } - /** - * @dev Register a name. - * @param id The token ID (keccak256 of the label). - * @param owner The address that should own the registration. - * @param duration Duration in seconds for the registration. - */ + /// @notice Register a name. + /// + /// @param id The token id determined by keccak256(label). + /// @param owner The address that should own the registration. + /// @param duration Duration in seconds for the registration. + /// + /// @return The expiry date of the registered name. function register(uint256 id, address owner, uint256 duration) external returns (uint256) { return _register(id, owner, duration, true); } - /** - * @dev Register a name and add details to the record in the Registry. - * @param id The token ID (keccak256 of the label). - * @param owner The address that should own the registration. - * @param duration Duration in seconds for the registration. - * @param resolver Address of the resolver for the name - * @param ttl time-to-live for the name - */ + /// @notice Register a name without modifying the Registry. + /// + /// @param id The token id determined by keccak256(label). + /// @param owner The address that should own the registration. + /// @param duration Duration in seconds for the registration. + /// + /// @return The expiry date of the registered name. + function registerOnly(uint256 id, address owner, uint256 duration) external returns (uint256) { + return _register(id, owner, duration, false); + } + + /// @notice Register a name and add details to the record in the Registry. + /// + /// @dev This method can only be called if: + /// 1. The contract is `live` + /// 2. The caller is an approved `controller` + /// 3. The name id is `available` + /// Emits `NameRegisteredWithRecord()` after successfully registering the name and setting the records. + /// + /// @param id The token id determined by keccak256(label). + /// @param owner The address that should own the registration. + /// @param duration Duration in seconds for the registration. + /// @param resolver Address of the resolver for the name. + /// @param ttl Time-to-live for the name. function registerWithRecord(uint256 id, address owner, uint256 duration, address resolver, uint64 ttl) external live @@ -146,42 +244,38 @@ contract BaseRegistrar is ERC721, Ownable { return expiry; } - /** - * @dev Register a name, without modifying the registry. - * @param id The token ID (keccak256 of the label). - * @param owner The address that should own the registration. - * @param duration Duration in seconds for the registration. - */ - function registerOnly(uint256 id, address owner, uint256 duration) external returns (uint256) { - return _register(id, owner, duration, false); - } - - /** - * @dev Gets the owner of the specified token ID. Names become unowned - * when their registration expires. - * @param tokenId uint256 ID of the token to query the owner of - * @return address currently marked as the owner of the given token ID - */ + /// @notice Gets the owner of the specified token ID. + /// + /// @dev Names become unowned when their registration expires. + /// + /// @param tokenId The id of the name to query the owner of. + /// + /// @return address The address currently marked as the owner of the given token ID. function ownerOf(uint256 tokenId) public view override returns (address) { if (expiries[tokenId] <= block.timestamp) revert Expired(tokenId); return super.ownerOf(tokenId); } - // Returns true iff the specified name is available for registration. + /// @notice Returns true if the specified name is available for registration. + /// + /// @param id The id of the name to check availability of. + /// + /// @return `true` if the name is available, else `false`. function isAvailable(uint256 id) public view returns (bool) { // Not available if it's registered here or in its grace period. return expiries[id] + GRACE_PERIOD < block.timestamp; } - /// @notice Allows holders of names can renew their ownerhsip and extend their expiry + /// @notice Allows holders of names to renew their ownerhsip and extend their expiry. /// /// @dev Renewal can be called while owning a subdomain or while the name is in the - /// @dev grace period. Can only be called by a controller. + /// grace period. Can only be called by a controller. + /// Emits `NameRenewed()` after renewing the name by updating the expiry. /// - /// @param id The Id to renew - /// @param duration The time that will be added to this name's expiry + /// @param id The id of the name to renew. + /// @param duration The time that will be added to this name's expiry. /// - /// @return The new expiry date + /// @return The new expiry date. function renew(uint256 id, uint256 duration) external live onlyController returns (uint256) { if (expiries[id] + GRACE_PERIOD < block.timestamp) revert NotRegisteredOrInGrace(id); @@ -190,14 +284,33 @@ contract BaseRegistrar is ERC721, Ownable { return expiries[id]; } - /** - * @dev Reclaim ownership of a name in ENS, if you own it in the registrar. - */ + /// @notice Reclaim ownership of a name in ENS, if you own it in the registrar. + /// + /// @dev Token transfers are ambiguous for determining name ownership transfers. This method exists so that + /// if a name token is transfered to a new owner, they have the right to claim ownership over their + /// name in the Registry. + /// + /// @param id The id of the name to reclaim. + /// @param owner The address of the owner that will be set in the Registry. function reclaim(uint256 id, address owner) external live { if (!_isApprovedOrOwner(msg.sender, id)) revert NotApprovedOwner(id, owner); ens.setSubnodeOwner(baseNode, bytes32(id), owner); } + /// @notice Register a name and possibly update the Registry. + /// + /// @dev This method can only be called if: + /// 1. The contract is `live` + /// 2. The caller is an approved `controller` + /// 3. The name id is `available` + /// Emits `NameRegistered()` after successfully registering the name. + /// + /// @param id The token id determined by keccak256(label). + /// @param owner The address that should own the registration. + /// @param duration Duration in seconds for the registration. + /// @param updateRegistry Whether to update the Regstiry with the ownership change + /// + /// @return The expiry date of the registered name. function _register(uint256 id, address owner, uint256 duration, bool updateRegistry) internal live @@ -213,6 +326,15 @@ contract BaseRegistrar is ERC721, Ownable { return expiry; } + /// @notice Internal handler for local state changes during registrations. + /// + /// @dev Sets the token's expiry time and then `burn`s and `mint`s a new token. + /// + /// @param id The token id determined by keccak256(label). + /// @param owner The address that should own the registration. + /// @param duration Duration in seconds for the registration. + /// + /// @return expiry The expiry date of the registered name. function _localRegister(uint256 id, address owner, uint256 duration) internal returns (uint256 expiry) { expiry = block.timestamp + duration; expiries[id] = expiry; @@ -223,15 +345,16 @@ contract BaseRegistrar is ERC721, Ownable { _mint(owner, id); } - /** - * v2.1.3 version of _isApprovedOrOwner which calls ownerOf(tokenId) and takes grace period into consideration instead of ERC721.ownerOf(tokenId); - * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v2.1.3/contracts/token/ERC721/ERC721.sol#L187 - * @dev Returns whether the given spender can transfer a given token ID - * @param spender address of the spender to query - * @param tokenId uint256 ID of the token to be transferred - * @return bool whether the msg.sender is approved for the given token ID, - * is an operator of the owner, or is the owner of the token - */ + /// @notice Returns whether the given spender can transfer a given token ID.abi + /// + /// @dev v2.1.3 version of _isApprovedOrOwner which calls ownerOf(tokenId) and takes grace period into consideration instead of ERC721.ownerOf(tokenId); + /// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v2.1.3/contracts/token/ERC721/ERC721.sol#L187 + /// + /// @param spender address of the spender to query + /// @param tokenId uint256 ID of the token to be transferred + /// + /// @return `true` if msg.sender is approved for the given token ID, is an operator of the owner, + /// or is the owner of the token, else `false`. function _isApprovedOrOwner(address spender, uint256 tokenId) internal view override returns (bool) { address owner = ownerOf(tokenId); return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender)); @@ -256,6 +379,14 @@ contract BaseRegistrar is ERC721, Ownable { return ""; } + /// @notice ERC165 compliant signal for interface support. + /// + /// @dev Checks interface support for reclaim OR IERC721 OR ERC165. + /// https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + /// + /// @param interfaceID the ERC165 iface id being checked for compliance + /// + /// @return bool Whether this contract supports the provided interfaceID function supportsInterface(bytes4 interfaceID) public pure override(ERC721) returns (bool) { return interfaceID == INTERFACE_META_ID || interfaceID == ERC721_ID || interfaceID == RECLAIM_ID; } diff --git a/src/L2/L2Resolver.sol b/src/L2/L2Resolver.sol index 8beb753c..fbf13b54 100644 --- a/src/L2/L2Resolver.sol +++ b/src/L2/L2Resolver.sol @@ -14,6 +14,15 @@ import {NameResolver} from "ens-contracts/resolvers/profiles/NameResolver.sol"; import {PubkeyResolver} from "ens-contracts/resolvers/profiles/PubkeyResolver.sol"; import {TextResolver} from "ens-contracts/resolvers/profiles/TextResolver.sol"; +/// @title L2 Resolver +/// +/// @notice The default resolver for the Base Usernames project. This contract implements the functionality of the ENS +/// PublicResolver while also inheriting ExtendedResolver for compatibility with CCIP-read. +/// Public Resolver: https://github.com/ensdomains/ens-contracts/blob/staging/contracts/resolvers/PublicResolver.sol +/// Extended Resolver: https://github.com/ensdomains/ens-contracts/blob/staging/contracts/resolvers/profiles/ExtendedResolver.sol +/// +/// @author Coinbase (https://github.com/base-org/usernames) +/// @author ENS (https://github.com/ensdomains/ens-contracts/tree/staging) contract L2Resolver is Multicallable, ABIResolver, @@ -30,44 +39,74 @@ contract L2Resolver is /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* STORAGE */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice The ENS registry. ENS public immutable ens; + + /// @notice The trusted registrar controller contract. address public registrarController; + + /// @notice The reverse registrar contract. address public reverseRegistrar; - /** - * A mapping of operators. An address that is authorised for an address - * may make any changes to the name that the owner could, but may not update - * the set of authorisations. - * (owner, operator) => approved - */ - mapping(address => mapping(address => bool)) private _operatorApprovals; - - /** - * A mapping of delegates. A delegate that is authorised by an owner - * for a name may make changes to the name's resolver, but may not update - * the set of token approvals. - * (owner, name, delegate) => approved - */ - mapping(address => mapping(bytes32 => mapping(address => bool))) private _tokenApprovals; + + /// @notice A mapping of operators per owner address. An operator is authroized to make changes to + /// all names owned by the `owner`. + mapping(address owner => mapping(address operator => bool isApproved)) private _operatorApprovals; + + /// @notice A mapping of delegates per owner per name (stored as a node). A delegate that is authorised + /// by an owner for a name may make changes to the name's resolver. + mapping(address owner => mapping(bytes32 node => mapping(address delegate => bool isApproved))) private + _tokenApprovals; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* ERRORS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Thown when msg.sender tries to set itself as an operator. error CantSetSelfAsOperator(); + + /// @notice Thrown when msg.sender tries to set itself as a delegate for one of its names. error CantSetSelfAsDelegate(); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* EVENTS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - // Logged when an operator is added or removed. + + /// @notice Emitted when an operator is added or removed. + /// + /// @param owner The address of the owner of names. + /// @param operator The address of the approved operator for the `owner`. + /// @param approved Whether the `operator` is approved or not. event ApprovalForAll(address indexed owner, address indexed operator, bool approved); - // Logged when a delegate is approved or an approval is revoked. + + /// @notice Emitted when a delegate is approved or an approval is revoked. + /// + /// @param owner The address of the owner of the name. + /// @param node The namehash of the name. + /// @param delegate The address of the operator for the specified `node`. + /// @param approved Whether the `delegate` is approved for the specified `node`. event Approved(address owner, bytes32 indexed node, address indexed delegate, bool indexed approved); + + /// @notice Emitted when the owner of this contract updates the Registrar Controller addrress. + /// + /// @param newRegistrarController The address of the new RegistrarController contract. event RegistrarControllerUpdated(address indexed newRegistrarController); + + /// @notice Emitted when the owner of this contract updates the Reverse Registrar address. + /// + /// @param newReverseRegistrar The address of the new ReverseRegistrar contract. event ReverseRegistrarUpdated(address indexed newReverseRegistrar); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* IMPLEMENTATION */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice L2 Resolver constructor used to establish the necessary contract configuration. + /// + /// @param ens_ The Registry contract. + /// @param registrarController_ The address of the RegistrarController contract. + /// @param reverseRegistrar_ The address of the ReverseRegistrar contract. + /// @param owner_ The permissioned address initialized as the `owner` in the `Ownable` context. constructor(ENS ens_, address registrarController_, address reverseRegistrar_, address owner_) { ens = ens_; registrarController = registrarController_; @@ -75,19 +114,27 @@ contract L2Resolver is _initializeOwner(owner_); } + /// @notice Allows the `owner` to set the registrar controller contract address. + /// + /// @dev Emits `RegistrarControllerUpdated` after setting the `registrarController` address. + /// + /// @param registrarController_ The address of the new RegistrarController contract. function setRegistrarController(address registrarController_) external onlyOwner { registrarController = registrarController_; emit RegistrarControllerUpdated(registrarController_); } + /// @notice Allows the `owner` to set the reverse registrar contract address. + /// + /// @dev Emits `ReverseRegistrarUpdated` after setting the `reverseRegistrar` address. + /// + /// @param reverseRegistrar_ The address of the new ReverseRegistrar contract. function setReverseRegistrar(address reverseRegistrar_) external onlyOwner { reverseRegistrar = reverseRegistrar_; emit ReverseRegistrarUpdated(reverseRegistrar_); } - /** - * @dev See {IERC1155-setApprovalForAll}. - */ + /// @dev See {IERC1155-setApprovalForAll}. function setApprovalForAll(address operator, bool approved) external { if (msg.sender == operator) revert CantSetSelfAsOperator(); @@ -95,16 +142,19 @@ contract L2Resolver is emit ApprovalForAll(msg.sender, operator, approved); } - /** - * @dev See {IERC1155-isApprovedForAll}. - */ + /// @dev See {IERC1155-isApprovedForAll}. function isApprovedForAll(address account, address operator) public view returns (bool) { return _operatorApprovals[account][operator]; } - /** - * @dev Approve a delegate to be able to updated records on a node. - */ + /// @notice Modify the permissions for a specified `delegate` for the specified `node`. + /// + /// @dev This method only sets the approval status for msg.sender's nodes. This is performed without checking + /// the ownership of the specified `node`. + /// + /// @param node The namehash `node` whose permissions are being updated. + /// @param delegate The address of the `delegate` + /// @param approved Whether the `delegate` has approval to modify records for `msg.sender`'s `node`. function approve(bytes32 node, address delegate, bool approved) external { if (msg.sender == delegate) revert CantSetSelfAsDelegate(); @@ -112,13 +162,30 @@ contract L2Resolver is emit Approved(msg.sender, node, delegate, approved); } - /** - * @dev Check to see if the delegate has been approved by the owner for the node. - */ + /// @notice Check to see if the `delegate` has been approved by the `owner` for the `node`. + /// + /// @param owner The address of the name owner. + /// @param node The namehash `node` whose permissions are being checked. + /// @param delegate The address of the `delegate` whose permissions are being checked. + /// + /// @return `true` if `delegate` is approved to modify `msg.sender`'s `node`, else `false`. function isApprovedFor(address owner, bytes32 node, address delegate) public view returns (bool) { return _tokenApprovals[owner][node][delegate]; } + /// @notice Check to see whether `msg.sender` is authroized to modify records for the specified `node`. + /// + /// @dev Override for `ResolverBase:isAuthorised()`. Used in the context of each inherited resolver "profile". + /// Validates that `msg.sender` is one of: + /// 1. The stored registrarController (for setting records upon registration) + /// 2 The stored reverseRegistrar (for setting reverse records) + /// 3. The owner of the node in the Registry + /// 4. An approved operator for owner + /// 5. An approved delegate for owner of the specified `node` + /// + /// @param node The namehashed `node` being authorized. + /// + /// @return `true` if `msg.sender` is authorized to modify records for the specified `node`, else `false`. function isAuthorised(bytes32 node) internal view override returns (bool) { if (msg.sender == registrarController || msg.sender == reverseRegistrar) { return true; @@ -127,6 +194,14 @@ contract L2Resolver is return owner == msg.sender || isApprovedForAll(owner, msg.sender) || isApprovedFor(owner, node, msg.sender); } + /// @notice ERC165 compliant signal for interface support. + /// + /// @dev Checks interface support for each inherited resolver profile + /// https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + /// + /// @param interfaceID the ERC165 iface id being checked for compliance + /// + /// @return bool Whether this contract supports the provided interfaceID function supportsInterface(bytes4 interfaceID) public view diff --git a/src/L2/RegistrarController.sol b/src/L2/RegistrarController.sol index 752bfbad..a16f1e3f 100644 --- a/src/L2/RegistrarController.sol +++ b/src/L2/RegistrarController.sol @@ -14,78 +14,204 @@ import {IPriceOracle} from "./interface/IPriceOracle.sol"; import {L2Resolver} from "./L2Resolver.sol"; import {IReverseRegistrar} from "./interface/IReverseRegistrar.sol"; -/** - * @dev A registrar controller for registering and renewing names at fixed cost. - */ +/// @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 RegistrarController is Ownable { 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; - uint256 discount; // denom in wei + /// @dev The discount value denominated in wei. + uint256 discount; } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* STORAGE */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice The implementation of the `BaseRegistrar`. BaseRegistrar immutable base; + + /// @notice The implementation of the pricing oracle. IPriceOracle public prices; + + /// @notice The implementation of the Reverse Registrar contract. IReverseRegistrar public reverseRegistrar; + + /// @notice An enumerable set for tracking which discounts are currently active. EnumerableSetLib.Bytes32Set internal activeDiscounts; + + /// @notice The node for which this name enables registration. It must match the `rootNode` of `base`. bytes32 public immutable rootNode; + + /// @notice The name for which this registration adds subdomains for, i.e. ".base.eth". string public rootName; - mapping(bytes32 => DiscountDetails) public discounts; - mapping(address => bool) public discountedRegistrants; + + /// @notice Each discount is stored against a unique 32-byte identifier, i.e. keccak256("test.discount.validator"). + mapping(bytes32 key => DiscountDetails details) public discounts; + + /// @notice Storage for which addresses have already registered with a discount. + mapping(address registrant => bool hasRegisteredWithDiscount) public discountedRegistrants; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* CONSTANTS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice The minimum registration duration, specified in seconds. uint256 public constant MIN_REGISTRATION_DURATION = 28 days; + + /// @notice The minimum name length. uint256 public constant MIN_NAME_LENGTH = 3; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* ERRORS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - error AlreadyClaimedWithDiscount(address sender); + + /// @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 the discount key does not match the specified details key. + /// + /// @param key The discount key that was to be used for the `discounts` mapping. + /// @param detailsKey The discount key that was going to be stored in the DiscountDetails. error DiscountKeyMismatch(bytes32 key, bytes32 detailsKey); + + /// @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. + /// @param amount The discount amount being set. error InvalidDiscountAmount(bytes32 key, uint256 amount); + + /// @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(); - error Unauthorised(bytes32 node); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* 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); - event PriceOracleUpdated(address indexed newPrices); + + /// @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 RegisteredWithDiscount(address indexed registrant, bytes32 indexed discountKey); - event ReverseRegistrarUpdated(address indexed newReverseRegistrar); + + /// @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(); @@ -99,8 +225,17 @@ contract RegistrarController is Ownable { _; } + /// @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) { - if (discountedRegistrants[msg.sender]) revert AlreadyClaimedWithDiscount(msg.sender); + if (discountedRegistrants[msg.sender]) revert AlreadyRegisteredWithDiscount(msg.sender); DiscountDetails memory details = discounts[discountKey]; if (!details.active) revert InactiveDiscount(discountKey); @@ -115,6 +250,17 @@ contract RegistrarController is Ownable { /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* IMPLEMENTATION */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Registrar Controller construction sets all of the requisite external contracts. + /// + /// @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. constructor( BaseRegistrar base_, IPriceOracle prices_, @@ -129,10 +275,19 @@ contract RegistrarController is Ownable { rootNode = rootNode_; rootName = rootName_; _initializeOwner(owner_); - // Assign ownership of this contract's reverse record to this contract's owner reverseRegistrar.claim(owner_); } + /// @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 key The uuid for the discount, i.e. keccak256("test.discount.validator"). + /// @param details The DiscountDetails for this discount key. function setDiscountDetails(bytes32 key, DiscountDetails memory details) external onlyOwner { if (details.discount == 0) revert InvalidDiscountAmount(key, details.discount); if (details.key != key) revert DiscountKeyMismatch(key, details.key); @@ -142,16 +297,31 @@ contract RegistrarController is Ownable { emit DiscountUpdated(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 { 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 { reverseRegistrar = reverse_; emit ReverseRegistrarUpdated(address(reverse_)); } + /// @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) public view returns (bool) { for (uint256 i; i < addresses.length; i++) { if (discountedRegistrants[addresses[i]]) { @@ -161,25 +331,57 @@ contract RegistrarController is Ownable { 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) && 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 = prices.price(name, base.nameExpires(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 @@ -190,6 +392,9 @@ contract RegistrarController is Ownable { 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) { bytes32[] memory activeDiscountKeys = activeDiscounts.values(); DiscountDetails[] memory activeDiscountDetails = new DiscountDetails[](activeDiscountKeys.length); @@ -199,6 +404,12 @@ contract RegistrarController is Ownable { 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); @@ -209,6 +420,17 @@ contract RegistrarController is Ownable { _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 @@ -227,6 +449,14 @@ contract RegistrarController is Ownable { emit RegisteredWithDiscount(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`. 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 { bytes32 labelhash = keccak256(bytes(name)); uint256 tokenId = uint256(labelhash); @@ -241,6 +471,11 @@ contract RegistrarController is Ownable { 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(); @@ -248,6 +483,13 @@ contract RegistrarController is Ownable { emit ETHPaymentProcessed(msg.sender, price); } + /// @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 { uint256 expires = base.registerWithRecord( uint256(keccak256(bytes(request.name))), request.owner, request.duration, request.resolver, 0 @@ -264,6 +506,9 @@ contract RegistrarController is Ownable { 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`. + /// + /// @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)}(""); @@ -271,31 +516,49 @@ contract RegistrarController is Ownable { } } + /// @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(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 { 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 { active ? activeDiscounts.add(key) : activeDiscounts.remove(key); } + /// @notice Allows anyone to withdraw the eth accumulated on this contract back to the `owner`. function withdrawETH() public { (bool sent,) = payable(owner()).call{value: (address(this).balance)}(""); if (!sent) revert TransferFailed(); } - /** - * @notice 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. - */ + /// @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); } diff --git a/src/L2/Registry.sol b/src/L2/Registry.sol index 02f77e43..40994bca 100644 --- a/src/L2/Registry.sol +++ b/src/L2/Registry.sol @@ -3,52 +3,85 @@ pragma solidity ^0.8.23; import {ENS} from "ens-contracts/registry/ENS.sol"; +/// @title Registry +/// +/// @notice Inspired by the ENS Registry contract. +/// https://github.com/ensdomains/ens-contracts/blob/staging/contracts/registry/ENSRegistry.sol +/// Stores names as `nodes` in a flat structure. Each registered `node` is assigned a `Record` struct. +/// +/// @author Coinbase (https://github.com/base-org/usernames) +/// @author ENS (https://github.com/ensdomains/ens-contracts/tree/staging) contract Registry is ENS { + /// @notice Structure for storing records on a per-node basis. struct Record { + /// @dev Tracks the owner of the node. address owner; + /// @dev Tracks the address of the resolver for that node. address resolver; + /// @dev The time-to-live for the node. uint64 ttl; } - mapping(bytes32 => Record) records; + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STORAGE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice The storage of `Record` structs per `node`. + mapping(bytes32 node => Record record) records; + + /// @notice Storage for approved operators on a per-holder basis. mapping(address nameHolder => mapping(address operator => bool isApproved)) operators; + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Thrown when the caller is not the owner or operator for a node. error Unauthorized(); - // Permits modifications only by the owner of the specified node. - modifier authorised(bytes32 node) { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* MODIFIERS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Decorator for permitting modifications only by the owner or operator of the specified node. + /// + /// @param node The node to check authorization approval for. + modifier authorized(bytes32 node) { address owner_ = records[node].owner; if (owner_ != msg.sender && !operators[owner_][msg.sender]) revert Unauthorized(); _; } - /** - * @dev Constructs a new ENS registry. - */ + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* IMPLEMENTATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Constructs a new Registry with the `rootOwner` as the permissioned address for managing establishing + /// TLD namespaces. + /// + /// @param rootOwner The address that can establish new TLDs. constructor(address rootOwner) { records[0x0].owner = rootOwner; } - /** - * @dev Sets the record for a node. - * @param node The node to update. - * @param owner_ The address of the new owner. - * @param resolver_ The address of the resolver. - * @param ttl_ The TTL in seconds. - */ + /// @notice Sets the record for a node. + /// + /// @param node The node to update. + /// @param owner_ The address of the new owner. + /// @param resolver_ The address of the resolver. + /// @param ttl_ The TTL in seconds. function setRecord(bytes32 node, address owner_, address resolver_, uint64 ttl_) external virtual override { setOwner(node, owner_); _setResolverAndTTL(node, resolver_, ttl_); } - /** - * @dev Sets the record for a subnode. - * @param node The parent node. - * @param label The hash of the label specifying the subnode. - * @param owner_ The address of the new owner. - * @param resolver_ The address of the resolver. - * @param ttl_ The TTL in seconds. - */ + /// @notice Sets the record for a subnode. + /// + /// @param node The parent node. + /// @param label The hash of the label specifying the subnode. + /// @param owner_ The address of the new owner. + /// @param resolver_ The address of the resolver. + /// @param ttl_ The TTL in seconds. function setSubnodeRecord(bytes32 node, bytes32 label, address owner_, address resolver_, uint64 ttl_) external virtual @@ -58,27 +91,30 @@ contract Registry is ENS { _setResolverAndTTL(subnode, resolver_, ttl_); } - /** - * @dev Transfers ownership of a node to a new address. May only be called by the current owner of the node. - * @param node The node to transfer ownership of. - * @param owner_ The address of the new owner. - */ - function setOwner(bytes32 node, address owner_) public virtual override authorised(node) { + /// @notice Transfers ownership of a node to a new address. + /// + /// @dev May only be called by the current owner or operator of the node. + /// + /// @param node The node to transfer ownership of. + /// @param owner_ The address of the new owner. + function setOwner(bytes32 node, address owner_) public virtual override authorized(node) { _setOwner(node, owner_); emit Transfer(node, owner_); } - /** - * @dev Transfers ownership of a subnode keccak256(node, label) to a new address. May only be called by the owner of the parent node. - * @param node The parent node. - * @param label The hash of the label specifying the subnode. - * @param owner_ The address of the new owner. - */ + /// @notice Transfers ownership of a subnode to a new address. + /// + /// @dev Subnode is determined by keccak256(node, label). + /// May only be called by the owner of the parent node. + /// + /// @param node The parent node. + /// @param label The hash of the label specifying the subnode. + /// @param owner_ The address of the new owner. function setSubnodeOwner(bytes32 node, bytes32 label, address owner_) public virtual override - authorised(node) + authorized(node) returns (bytes32) { bytes32 subnode = keccak256(abi.encodePacked(node, label)); @@ -87,92 +123,103 @@ contract Registry is ENS { return subnode; } - /** - * @dev Sets the resolver address for the specified node. - * @param node The node to update. - * @param resolver_ The address of the resolver. - */ - function setResolver(bytes32 node, address resolver_) public virtual override authorised(node) { + /// @notice Sets the resolver address for the specified node. + /// + /// @dev May only be called by the current owner or operator of the node. + /// + /// @param node The node to update. + /// @param resolver_ The address of the resolver. + function setResolver(bytes32 node, address resolver_) public virtual override authorized(node) { records[node].resolver = resolver_; emit NewResolver(node, resolver_); } - /** - * @dev Sets the TTL for the specified node. - * @param node The node to update. - * @param ttl_ The TTL in seconds. - */ - function setTTL(bytes32 node, uint64 ttl_) public virtual override authorised(node) { + /// @notice Sets the TTL for the specified node. + /// + /// @dev May only be called by the current owner or operator of the node. + /// + /// @param node The node to update. + /// @param ttl_ The TTL in seconds. + function setTTL(bytes32 node, uint64 ttl_) public virtual override authorized(node) { records[node].ttl = ttl_; emit NewTTL(node, ttl_); } - /** - * @dev Enable or disable approval for a third party ("operator") to manage - * all of `msg.sender`'s ENS records. Emits the ApprovalForAll event. - * @param operator Address to add to the set of authorized operators. - * @param approved True if the operator is approved, false to revoke approval. - */ + /// @notice Set `operator`'s approval status for msg.sender. + /// + /// @dev Enable or disable approval for a third party ("operator") to manage + /// all of `msg.sender`'s ENS records. Emits the `ApprovalForAll()` event. + /// + /// @param operator Address to add to the set of authorized operators. + /// @param approved True if the operator is approved, false to revoke approval. function setApprovalForAll(address operator, bool approved) external virtual override { operators[msg.sender][operator] = approved; emit ApprovalForAll(msg.sender, operator, approved); } - /** - * @dev Returns the address that owns the specified node. - * @param node The specified node. - * @return address of the owner. - */ + /// @notice Returns the address that owns the specified node. + /// + /// @param node The specified node. + /// + /// @return The address for the specified node if one is set, returns address(0) if this contract is owner. function owner(bytes32 node) public view virtual override returns (address) { address addr = records[node].owner; if (addr == address(this)) { - return address(0x0); + return address(0); } - return addr; } - /** - * @dev Returns the address of the resolver for the specified node. - * @param node The specified node. - * @return address of the resolver. - */ + /// @notice Returns the address of the resolver for the specified node. + /// + /// @param node The specified node. + /// + /// @return The address of the resolver. function resolver(bytes32 node) public view virtual override returns (address) { return records[node].resolver; } - /** - * @dev Returns the TTL of a node, and any records associated with it. - * @param node The specified node. - * @return ttl of the node. - */ + /// @notice Returns the TTL of a node. + /// + /// @param node The specified node. + /// + /// @return The ttl of the node. function ttl(bytes32 node) public view virtual override returns (uint64) { return records[node].ttl; } - /** - * @dev Returns whether a record has been imported to the registry. - * @param node The specified node. - * @return Bool if record exists - */ + /// @notice Returns whether a record exists in this registry. + /// + /// @param node The specified node. + /// + /// @return `true` if a record exists, else `false`. function recordExists(bytes32 node) public view virtual override returns (bool) { return records[node].owner != address(0x0); } - /** - * @dev Query if an address is an authorized operator for another address. - * @param owner_ The address that owns the records. - * @param operator The address that acts on behalf of the owner. - * @return True if `operator` is an approved operator for `owner`, false otherwise. - */ + /// @notice Query if an address is an authorized operator for another address. + /// + /// @param owner_ The address that owns the records. + /// @param operator The address that acts on behalf of the owner. + /// + /// @return `true` if `operator` is an approved operator for `owner`, else `fase`. function isApprovedForAll(address owner_, address operator) external view virtual override returns (bool) { return operators[owner_][operator]; } + /// @notice Set the owner in storage. + /// + /// @param node The specified node. + /// @param owner_ The owner to store for that node. function _setOwner(bytes32 node, address owner_) internal virtual { records[node].owner = owner_; } + /// @notice Set the resolver and ttl in storage. + /// + /// @param node The spcified node. + /// @param resolver_ The address of the resolver. + /// @param ttl_ The TTL in seconds. function _setResolverAndTTL(bytes32 node, address resolver_, uint64 ttl_) internal { if (resolver_ != records[node].resolver) { records[node].resolver = resolver_; diff --git a/src/L2/ReverseRegistrar.sol b/src/L2/ReverseRegistrar.sol index c5a3a43a..5485dfc6 100644 --- a/src/L2/ReverseRegistrar.sol +++ b/src/L2/ReverseRegistrar.sol @@ -8,28 +8,65 @@ import {Ownable} from "solady/auth/Ownable.sol"; import {ADDR_REVERSE_NODE} from "src/util/Constants.sol"; import {Sha3} from "src/lib/Sha3.sol"; +/// @title Reverse Registrar +/// +/// @notice Registrar which allows registrants to establish a name as their "primary" record for reverse resolution. +/// Inspired by ENS's ReverseRegistrar implementation: +/// https://github.com/ensdomains/ens-contracts/blob/staging/contracts/reverseRegistrar/ReverseRegistrar.sol +/// +/// @author Coinbase (https://github.com/base-org/usernames) +/// @author ENS (https://github.com/ensdomains/ens-contracts) contract ReverseRegistrar is Ownable { /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* STORAGE */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice The ENS registry. ENS public immutable ens; + + /// @notice The default resolver for setting Name resolution records. NameResolver public defaultResolver; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* ERRORS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Thrown when `sender` is not authrorized to modify records for `addr`. + /// + /// @param addr The `addr` that was being modified. + /// @param sender The unauthorized sender. error NotAuthorized(address addr, address sender); + + /// @notice Thrown when trying to set the zero address as the default resolver. error NoZeroAddress(); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* EVENTS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Emitted upon successfully establishing a reverse record. + /// + /// @param addr The address for which the the record was set. + /// @param node The namehashed node that was set as the reverse record. event ReverseClaimed(address indexed addr, bytes32 indexed node); + + /// @notice Emitted when the default Resolver is changed by the `owner`. + /// + /// @param resolver The address of the new Resolver. event DefaultResolverChanged(NameResolver indexed resolver); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* MODIFIERS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice Decorator for checking authorization status for a caller against a provided `addr`. + /// + /// @dev A caller is authorized to set the record for `addr` if they are one of: + /// 1. The `addr` is the sender + /// 2. The sender is an approved operator for `addr` on the registry + /// 3. The sender is `Ownable:ownerOf()` for `addr` + /// + /// @param addr The `addr` that is being modified. modifier authorized(address addr) { if (addr != msg.sender && !ens.isApprovedForAll(addr, msg.sender) && !_ownsContract(addr)) { revert NotAuthorized(addr, msg.sender); @@ -40,40 +77,47 @@ contract ReverseRegistrar is Ownable { /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* IMPLEMENTATION */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - /** - * @dev Constructor - * @param ens_ The address of the ENS registry. - * @param owner_ The owner of the contract - */ + + /// @notice ReverseRegistrar construction. + /// + /// @param ens_ The ENS registry, will be stored as `ens`. + /// @param owner_ The permissioned address initialized as the `owner` in the `Ownable` context. constructor(ENS ens_, address owner_) { _initializeOwner(owner_); ens = ens_; } + /// @notice Allows the owner to change the address of the default resolver. + /// + /// @dev The address of the new `resolver` must not be the zero address. + /// Emits `DefaultResolverChanged` after successfully storing `resolver` as `defaultResolver`. + /// + /// @param resolver The address of the new resolver. function setDefaultResolver(address resolver) public onlyOwner { if (address(resolver) == address(0)) revert NoZeroAddress(); defaultResolver = NameResolver(resolver); emit DefaultResolverChanged(defaultResolver); } - /** - * @dev Transfers ownership of the reverse ENS record associated with the - * calling account. - * @param owner The address to set as the owner of the reverse record in ENS. - * @return The ENS node hash of the reverse record. - */ + /// @notice Transfers ownership of the reverse ENS record for `msg.sender` to the provided `owner`. + /// + /// @param owner The address to set as the owner of the reverse record in ENS. + /// + /// @return The ENS node hash of the reverse record. function claim(address owner) public returns (bytes32) { return claimForAddr(msg.sender, owner, address(defaultResolver)); } - /** - * @dev Transfers ownership of the reverse ENS record associated with the - * calling account. - * @param addr The reverse record to set - * @param owner The address to set as the owner of the reverse record in ENS. - * @param resolver The resolver of the reverse node - * @return The ENS node hash of the reverse record. - */ + /// @notice Transfers ownership of the reverse ENS record for `addr` to the provided `owner`. + /// + /// @dev Restricted to only `authorized` owners/operators of `addr`. + /// Emits `ReverseClaimed` after successfully transfering ownership of the reverse record. + /// + /// @param addr The reverse record to set. + /// @param owner The new owner of the reverse record in ENS. + /// @param resolver The address of the resolver to set. + /// + /// @return The ENS node hash of the reverse record. function claimForAddr(address addr, address owner, address resolver) public authorized(addr) returns (bytes32) { bytes32 labelHash = Sha3.hexAddress(addr); bytes32 reverseNode = keccak256(abi.encodePacked(ADDR_REVERSE_NODE, labelHash)); @@ -82,38 +126,37 @@ contract ReverseRegistrar is Ownable { return reverseNode; } - /** - * @dev Transfers ownership of the reverse ENS record associated with the - * calling account. - * @param owner The address to set as the owner of the reverse record in ENS. - * @param resolver The address of the resolver to set; 0 to leave unchanged. - * @return The ENS node hash of the reverse record. - */ + /// @notice Transfers ownership and sets the resolver of the reverse ENS record for `addr` to the provided `owner`. + /// + /// @param owner The address to set as the owner of the reverse record in ENS. + /// @param resolver The address of the resolver to set. + /// + /// @return The ENS node hash of the reverse record. function claimWithResolver(address owner, address resolver) public returns (bytes32) { return claimForAddr(msg.sender, owner, resolver); } - /** - * @dev Sets the `name()` record for the reverse ENS record associated with - * the calling account. First updates the resolver to the default reverse - * resolver if necessary. - * @param name The name to set for this address. - * @return The ENS node hash of the reverse record. - */ + /// @notice Set the `name()` record for the reverse ENS record associated with the calling account. + /// + /// @dev This call will first updates the resolver to the default reverse resolver if necessary. + /// + /// @param name The name to set for msg.sender. + /// + /// @return The ENS node hash of the reverse record. function setName(string memory name) public returns (bytes32) { return setNameForAddr(msg.sender, msg.sender, address(defaultResolver), name); } - /** - * @dev Sets the `name()` record for the reverse ENS record associated with - * the account provided. Updates the resolver to a designated resolver - * Only callable by controllers and authorised users - * @param addr The reverse record to set - * @param owner The owner of the reverse node - * @param resolver The resolver of the reverse node - * @param name The name to set for this address. - * @return The ENS node hash of the reverse record. - */ + /// @notice Sets the `name()` record for the reverse ENS record associated with the `addr` provided. + /// + /// @dev Updates the resolver to a designated resolver. Only callable by `addr`'s `authroized` addresses. + /// + /// @param addr The reverse record to set. + /// @param owner The owner of the reverse node. + /// @param resolver The resolver of the reverse node. + /// @param name The name to set for this address. + /// + /// @return The ENS node hash of the reverse record. function setNameForAddr(address addr, address owner, address resolver, string memory name) public returns (bytes32) @@ -123,15 +166,21 @@ contract ReverseRegistrar is Ownable { return node_; } - /** - * @dev Returns the node hash for a given account's reverse records. - * @param addr The address to hash - * @return The ENS node hash. - */ + /// @notice Returns the node hash for a provided `addr`'s reverse records. + /// + /// @param addr The address to hash. + /// + /// @return The ENS node hash. function node(address addr) public pure returns (bytes32) { return keccak256(abi.encodePacked(ADDR_REVERSE_NODE, Sha3.hexAddress(addr))); } + /// @notice Allows this contract to check if msg.sender is the `Ownable:owner()` for `addr`. + /// + /// @dev First checks if `addr` is a contract and returns early if not. Then uses a `try/except` to + /// see if `addr` responds with a valid address. + /// + /// @return `true` if the address returned from `Ownable:owner()` == msg.sender, else `false`. function _ownsContract(address addr) internal view returns (bool) { // Determine if a contract exists at `addr` and return early if not if (!_isContract(addr)) { @@ -145,6 +194,11 @@ contract ReverseRegistrar is Ownable { } } + /// @notice Check if the provided `addr` has a nonzero `extcodesize`. + /// + /// @param addr The address to check. + /// + /// @return `true` if `extcodesize` > 0, else `false`. function _isContract(address addr) internal view returns (bool) { uint32 size; assembly { diff --git a/src/L2/StablePriceOracle.sol b/src/L2/StablePriceOracle.sol index 1b45b8aa..ea27e384 100644 --- a/src/L2/StablePriceOracle.sol +++ b/src/L2/StablePriceOracle.sol @@ -5,18 +5,42 @@ import {StringUtils} from "ens-contracts/ethregistrar/StringUtils.sol"; import {IPriceOracle} from "src/L2/interface/IPriceOracle.sol"; -// StablePriceOracle sets a price in wei +/// @title Stable Pricing Oracle +/// +/// @notice The pricing mechanism for setting the "base price" of names on a per-letter basis. +/// Inspired by the ENS StablePriceOracle contract: +/// https://github.com/ensdomains/ens-contracts/blob/staging/contracts/ethregistrar/StablePriceOracle.sol +/// +/// @author Coinbase (https://github.com/base-org/usernames) +/// @author ENS (https://github.com/ensdomains/ens-contracts) contract StablePriceOracle is IPriceOracle { using StringUtils for *; - // Rent in wei by length + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STORAGE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @notice The price for a 1 letter name per second. uint256 public immutable price1Letter; + + /// @notice The price for a 2 letter name per second. uint256 public immutable price2Letter; + + /// @notice The price for a 3 letter name per second. uint256 public immutable price3Letter; + + /// @notice The price for a 4 letter name per second. uint256 public immutable price4Letter; + + /// @notice The price for a 5 to 9 letter name per second. uint256 public immutable price5Letter; + + /// @notice The price for a 10 or longer letter name per second. uint256 public immutable price10Letter; + /// @notice Price Oracle constructor which sets the immutably stored prices. + /// + /// @param _rentPrices An array of prices orderd in increasing length. constructor(uint256[] memory _rentPrices) { price1Letter = _rentPrices[0]; price2Letter = _rentPrices[1]; @@ -26,13 +50,13 @@ contract StablePriceOracle is IPriceOracle { price10Letter = _rentPrices[5]; } - /** - * @dev Returns the price to register or renew a name. - * @param name The name being registered or renewed. - * @param expires When the name presently expires (0 if this is a new registration). - * @param duration How long the name is being registered or extended for, in seconds. - * @return base premium tuple of base price + premium price - */ + /// @notice Returns the price to register or renew a name given an expiry and duration. + /// + /// @param name The name being registered or renewed. + /// @param expires When the name presently expires (0 if this is a new registration). + /// @param duration How long the name is being registered or extended for, in seconds. + /// + /// @return A `Price` tuple of `basePrice` and `premiumPrice`. function price(string calldata name, uint256 expires, uint256 duration) external view @@ -58,16 +82,12 @@ contract StablePriceOracle is IPriceOracle { return IPriceOracle.Price({base: basePrice, premium: premium_}); } - /** - * @dev Returns the pricing premium in wei. - */ + /// @notice Returns the pricing premium denominated in wei. function premium(string calldata name, uint256 expires, uint256 duration) external view returns (uint256) { return _premium(name, expires, duration); } - /** - * @dev Returns the pricing premium in internal base units. - */ + /// @notice Returns the pricing premium denominated in wei. function _premium(string memory, uint256, uint256) internal view virtual returns (uint256) { return 0; } diff --git a/test/RegistrarController/DiscountedRegister.t.sol b/test/RegistrarController/DiscountedRegister.t.sol index 0ea1dcfc..e7245623 100644 --- a/test/RegistrarController/DiscountedRegister.t.sol +++ b/test/RegistrarController/DiscountedRegister.t.sol @@ -119,4 +119,23 @@ contract DiscountedRegister is RegistrarControllerBase { uint256 expectedBalance = 1 ether - price; assertEq(user.balance, expectedBalance); } + + function test_reverts_ifTheRegistrantHasAlreadyRegisteredWithDiscount() public { + vm.deal(user, 1 ether); + vm.prank(owner); + controller.setDiscountDetails(discountKey, _getDefaultDiscount()); + uint256 price = controller.discountedRegisterPrice(name, duration, discountKey); + validator.setReturnValue(true); + base.setAvailable(uint256(nameLabel), true); + RegistrarController.RegisterRequest memory request = _getDefaultRegisterRequest(); + uint256 expires = block.timestamp + request.duration; + base.setNameExpires(uint256(nameLabel), expires); + vm.prank(user); + controller.discountedRegister{value: price}(request, discountKey, ""); + + vm.expectRevert(abi.encodeWithSelector(RegistrarController.AlreadyRegisteredWithDiscount.selector, user)); + request.name = "newname"; + vm.prank(user); + controller.discountedRegister{value: price}(request, discountKey, ""); + } }