-
Notifications
You must be signed in to change notification settings - Fork 65
Add ReverseRegistrarV2 #125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 7 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
e167c88
New ReverseRegistrarV2 contract with owner only fwd resolution writes
stevieraykatz 18066f5
lint
stevieraykatz 8d95036
Add natspec for copied helpers
stevieraykatz acc0056
Bring scope of ReverseRegistrarShim into ReverseRegistrarV2
stevieraykatz 7d3e385
Unit tests
stevieraykatz e19b6f6
chore: lint
stevieraykatz b897117
chore: address PR comments
stevieraykatz 64214a9
feat: fix bug where _bytesToAddress reverts for zero address
stevieraykatz 5d95c7a
feat: add unit tests for setBaseForwardAddr
stevieraykatz db4bfe4
chore: remove unused helper
stevieraykatz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,321 @@ | ||
//SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.23; | ||
|
||
import {AddrResolver} from "ens-contracts/resolvers/profiles/AddrResolver.sol"; | ||
import {ENS} from "ens-contracts/registry/ENS.sol"; | ||
import {IL2ReverseRegistrar} from "src/L2/interface/IL2ReverseRegistrar.sol"; | ||
import {NameResolver} from "ens-contracts/resolvers/profiles/NameResolver.sol"; | ||
import {Ownable} from "solady/auth/Ownable.sol"; | ||
|
||
import {Sha3} from "src/lib/Sha3.sol"; | ||
|
||
/// @title Reverse Registrar V2 | ||
/// | ||
/// @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 | ||
/// Writes records to the network-specific reverse node set on construction via `reverseNode`. | ||
/// Compliant with ENSIP-19: https://docs.ens.domains/ensip/19 | ||
/// | ||
/// @author Coinbase (https://github.com/base/basenames) | ||
contract ReverseRegistrarV2 is Ownable { | ||
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | ||
/* STORAGE */ | ||
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | ||
|
||
/// @notice The Registry contract. | ||
ENS public immutable registry; | ||
|
||
/// @notice The reverse node this registrar manages. | ||
bytes32 public immutable reverseNode; | ||
|
||
/// @notice The address of the ENS-deployed L2ReverseRegistrar contract. | ||
address public immutable l2ReverseRegistrar; | ||
|
||
/// @notice The network cointype. | ||
uint256 public immutable cointype; | ||
|
||
/// @notice Permissioned controller contracts. | ||
mapping(address controller => bool approved) public controllers; | ||
|
||
/// @notice The default resolver for setting Name resolution records. | ||
address public defaultResolver; | ||
|
||
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | ||
/* ERRORS */ | ||
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | ||
|
||
/// @notice Thrown when `sender` is not authorized 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 base-specific reverse record. | ||
/// | ||
/// @param addr The address for which the the record was set. | ||
/// @param node The namehashed node that was set as the base reverse record. | ||
event BaseReverseClaimed(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(address indexed resolver); | ||
|
||
/// @notice Emitted when a controller address approval status is changed by the `owner`. | ||
/// | ||
/// @param controller The address of the `controller`. | ||
/// @param approved The new approval state for the `controller` address. | ||
event ControllerApprovalChanged(address indexed controller, bool approved); | ||
|
||
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | ||
/* 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 `controller` | ||
/// 3. The sender is an approved operator for `addr` on the registry | ||
/// 4. The sender is `Ownable:ownerOf()` for `addr` | ||
/// | ||
/// @param addr The `addr` that is being modified. | ||
modifier authorized(address addr) { | ||
if ( | ||
addr != msg.sender && !controllers[msg.sender] && !registry.isApprovedForAll(addr, msg.sender) | ||
&& !_ownsContract(addr) | ||
) { | ||
revert NotAuthorized(addr, msg.sender); | ||
} | ||
_; | ||
} | ||
|
||
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | ||
/* IMPLEMENTATION */ | ||
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | ||
|
||
/// @notice ReverseRegistrar construction. | ||
/// | ||
/// @param registry_ The ENS registry, will be stored as `registry`. | ||
/// @param owner_ The permissioned address initialized as the `owner` in the `Ownable` context. | ||
/// @param reverseNode_ The network-sepcific reverse node. | ||
/// @param cointype_ The network-specific cointype. | ||
constructor(ENS registry_, address owner_, bytes32 reverseNode_, address l2ReverseRegistrar_, uint256 cointype_) { | ||
_initializeOwner(owner_); | ||
registry = registry_; | ||
reverseNode = reverseNode_; | ||
l2ReverseRegistrar = l2ReverseRegistrar_; | ||
cointype = cointype_; | ||
} | ||
|
||
/// @notice Sets the reverse record `name` for `addr`. | ||
/// | ||
/// @dev First calls the ENS L2ReverseRegistrar and sets the name-record for the provided address. Then follows the legacy | ||
/// Basenames reverse registrar flow. The call to the L2ReverseRegistrar will revert if the signature is invalid or | ||
/// if the signature is expired. | ||
/// | ||
/// @param addr The name records will be set for this address. | ||
/// @param signatureExpiry The timestamp expiration of the signature. | ||
/// @param name The name that will be stored for `addr`. | ||
/// @param cointypes The array of networks-as-cointypes used in replayable reverse sets. | ||
/// @param signature The signature bytes. | ||
function setNameForAddrWithSignature( | ||
address addr, | ||
uint256 signatureExpiry, | ||
string calldata name, | ||
uint256[] memory cointypes, | ||
bytes memory signature | ||
) external returns (bytes32) { | ||
IL2ReverseRegistrar(l2ReverseRegistrar).setNameForAddrWithSignature( | ||
ilikesymmetry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
addr, signatureExpiry, name, cointypes, signature | ||
); | ||
return setNameForAddr(addr, msg.sender, defaultResolver, name); | ||
} | ||
|
||
|
||
/// @notice Allows the owner to back populate the ENSIP-11 forward resolution records for basenames. | ||
/// | ||
/// @dev For each node in `nodes` we make a series of checks to make sure that we're | ||
/// 1. Trying to set data against our L2Resolver contract. | ||
/// 2. Setting a valid forward resolution address. | ||
/// 3. Not overwriting an existing value. | ||
/// If any of these checks fails, we skip this node and continue. | ||
/// | ||
/// @param nodes The array of nodes for which records will be set. | ||
function setBaseForwardAddr(bytes32[] memory nodes) public onlyOwner { | ||
ilikesymmetry marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for (uint256 i; i < nodes.length; i++) { | ||
bytes32 _node = nodes[i]; | ||
|
||
// Get the resolver address for the node and check that it is our public resolver. | ||
address resolverAddr = registry.resolver(_node); | ||
if (resolverAddr != defaultResolver) continue; | ||
AddrResolver resolver = AddrResolver(resolverAddr); | ||
|
||
// Get the `addr` record for the node and check validity. | ||
address resolvedAddr = resolver.addr(_node); | ||
if (resolvedAddr == address(0)) continue; | ||
|
||
// Check if there is an ENSIP-11 cointype address already set for this node. | ||
if (_bytesToAddress(resolver.addr(_node, cointype)) != address(0)) continue; | ||
|
||
// Set the ENSIP-11 forward resolution addr. | ||
resolver.setAddr(_node, cointype, _addressToBytes(resolvedAddr)); | ||
} | ||
} | ||
|
||
/// @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 = resolver; | ||
registry.setResolver(reverseNode, resolver); | ||
emit DefaultResolverChanged(defaultResolver); | ||
} | ||
|
||
/// @notice Allows the owner to change the approval status of an address as a controller. | ||
/// | ||
/// @param controller The address of the controller. | ||
/// @param approved Whether the controller has permissions to modify reverse records. | ||
function setControllerApproval(address controller, bool approved) public onlyOwner { | ||
if (controller == address(0)) revert NoZeroAddress(); | ||
controllers[controller] = approved; | ||
emit ControllerApprovalChanged(controller, approved); | ||
} | ||
|
||
/// @notice Transfers ownership of the base-specific 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 base-specific reverse record. | ||
function claim(address owner) public returns (bytes32) { | ||
return claimForBaseAddr(msg.sender, owner, defaultResolver); | ||
} | ||
|
||
/// @notice Transfers ownership of the base-specific reverse ENS record for `addr` to the provided `owner`. | ||
/// | ||
/// @dev Restricted to only `authorized` owners/operators of `addr`. | ||
/// Emits `BaseReverseClaimed` 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 base-specific reverse record. | ||
function claimForBaseAddr(address addr, address owner, address resolver) | ||
public | ||
authorized(addr) | ||
returns (bytes32) | ||
{ | ||
bytes32 labelHash = Sha3.hexAddress(addr); | ||
bytes32 baseReverseNode = keccak256(abi.encodePacked(reverseNode, labelHash)); | ||
emit BaseReverseClaimed(addr, baseReverseNode); | ||
registry.setSubnodeRecord(reverseNode, labelHash, owner, resolver, 0); | ||
return baseReverseNode; | ||
} | ||
|
||
/// @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 base-specific reverse record. | ||
function claimWithResolver(address owner, address resolver) public returns (bytes32) { | ||
return claimForBaseAddr(msg.sender, owner, resolver); | ||
} | ||
|
||
/// @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); | ||
} | ||
|
||
/// @notice Sets the `name()` record for the reverse ENS records 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 `baseAsCoinType.reverse` record. | ||
function setNameForAddr(address addr, address owner, address resolver, string memory name) | ||
public | ||
returns (bytes32) | ||
{ | ||
bytes32 baseNode_ = claimForBaseAddr(addr, owner, resolver); | ||
NameResolver(resolver).setName(baseNode_, name); | ||
|
||
return baseNode_; | ||
} | ||
|
||
/// @notice Returns the node hash for a provided `addr`'s reverse records. | ||
/// | ||
/// @param addr The address to hash. | ||
/// | ||
/// @return The base-specific reverse node hash. | ||
function node(address addr) public view returns (bytes32) { | ||
return keccak256(abi.encodePacked(reverseNode, 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 (addr.code.length == 0) { | ||
return false; | ||
} | ||
// If a contract does exist, try and call `Ownable.owner()` | ||
try Ownable(addr).owner() returns (address owner) { | ||
return owner == msg.sender; | ||
} catch { | ||
return false; | ||
} | ||
} | ||
|
||
/// @notice Helper for converting an address stored as bytes into an address type. | ||
/// | ||
/// @dev Copied from ENS `AddrResolver`: | ||
/// https://github.com/ensdomains/ens-contracts/blob/staging/contracts/resolvers/profiles/AddrResolver.sol | ||
/// | ||
/// @param b Address bytes. | ||
function _bytesToAddress(bytes memory b) internal pure returns (address payable a) { | ||
require(b.length == 20); | ||
assembly { | ||
a := div(mload(add(b, 32)), exp(256, 12)) | ||
} | ||
} | ||
|
||
|
||
/// @notice Helper for converting an address into a bytes object. | ||
/// | ||
/// @dev Copied from ENS `AddrResolver`: | ||
/// https://github.com/ensdomains/ens-contracts/blob/staging/contracts/resolvers/profiles/AddrResolver.sol | ||
/// | ||
/// @param a Address. | ||
function _addressToBytes(address a) internal pure returns (bytes memory b) { | ||
b = new bytes(20); | ||
assembly { | ||
mstore(add(b, 32), mul(a, exp(256, 12))) | ||
} | ||
} | ||
|
||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.4; | ||
|
||
/// @notice Interface for the L2 Reverse Registrar. | ||
/// https://github.com/ensdomains/ens-contracts/pull/265 | ||
interface IL2ReverseRegistrar { | ||
/// @notice Sets the `nameForAddr()` record for the calling account. | ||
/// | ||
/// @param name The name to set. | ||
function setName(string memory name) external; | ||
|
||
/// @notice Sets the `nameForAddr()` record for the addr provided account. | ||
/// | ||
/// @param addr The address to set the name for. | ||
/// @param name The name to set. | ||
function setNameForAddr(address addr, string memory name) external; | ||
|
||
/// @notice Sets the `nameForAddr()` record for the addr provided account using a signature. | ||
/// | ||
/// @param addr The address to set the name for. | ||
/// @param signatureExpiry Date when the signature expires. | ||
/// @param name The name to set. | ||
/// @param coinTypes The coin types to set. Must be inclusive of the coin type for the contract. | ||
/// @param signature The signature from the addr. | ||
function setNameForAddrWithSignature( | ||
address addr, | ||
uint256 signatureExpiry, | ||
string memory name, | ||
uint256[] memory coinTypes, | ||
bytes memory signature | ||
) external; | ||
|
||
/// @notice Sets the `nameForAddr()` record for the contract provided that is owned with `Ownable`. | ||
/// | ||
/// @param contractAddr The address of the contract to set the name for (implementing Ownable). | ||
/// @param owner The owner of the contract (via Ownable). | ||
/// @param signatureExpiry The expiry of the signature. | ||
/// @param name The name to set. | ||
/// @param coinTypes The coin types to set. Must be inclusive of the coin type for the contract. | ||
/// @param signature The signature of an address that will return true on isValidSignature for the owner. | ||
function setNameForOwnableWithSignature( | ||
address contractAddr, | ||
address owner, | ||
uint256 signatureExpiry, | ||
string memory name, | ||
uint256[] memory coinTypes, | ||
bytes memory signature | ||
) external; | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.