-
Notifications
You must be signed in to change notification settings - Fork 51
Upgradeable L2 Resolver #98
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
Open
stevieraykatz
wants to merge
26
commits into
main
Choose a base branch
from
upgradeable-l2-resolver
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
11eda3e
First pass at upgradeable L2 Resolver with EIP-7201 namespaced storage
stevieraykatz 83dbacb
Move UpgradeableL2Resolver to dir
stevieraykatz f1e9a73
Migrate L2 Resolver tests to upgradeable
stevieraykatz fb6f8b9
ABI Resolver tests
stevieraykatz 362ad95
Cleanup tests, add user-owned/managed node for resolver profile tests
stevieraykatz 5a55934
Fix pragmas
stevieraykatz 9f1cf35
lint
stevieraykatz 359b3fb
Finish Addr Resolver + unit tests
stevieraykatz 845f497
Add contenthash tests, natspec, add auth check to existing unit tests
stevieraykatz eb8760a
fix testfile name
stevieraykatz 953172f
Cleanup DNSResolver, start tests for it
stevieraykatz 4b12718
Cleanup natspec in InterfaceResolver, add unit tests
stevieraykatz 8928c15
lint
stevieraykatz d9ce766
Name Resolver natspec and unit tests
stevieraykatz d54b4b4
Pubkey Resolver natspec and test
stevieraykatz 09a52e9
Text resolver natspec and tests
stevieraykatz f344d26
lint
stevieraykatz 21ceab2
Cleanup to UpgradeableL2Resolver natspec
stevieraykatz 65c58a8
Finish DNS record tests
stevieraykatz 90710d0
feat: add `notProxyAdmin` modifier for fuzz tests
abdulla-cb feb5e22
feat: add view methods for registrarController and reverseRegistrar
abdulla-cb 00e3514
test: add additional cases
abdulla-cb d2c06eb
test: add tests for versionable resolver
abdulla-cb ff94866
test: setAddr reverts if invalid
abdulla-cb 64ad9e1
test: add DNS records test
abdulla-cb c5145ae
chore: fix typo
abdulla-cb 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,256 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.23; | ||
|
||
import {ENS} from "ens-contracts/registry/ENS.sol"; | ||
import {ExtendedResolver} from "ens-contracts/resolvers/profiles/ExtendedResolver.sol"; | ||
import {IExtendedResolver} from "ens-contracts/resolvers/profiles/IExtendedResolver.sol"; | ||
import {Initializable} from "lib/openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; | ||
import {Multicallable} from "ens-contracts/resolvers/Multicallable.sol"; | ||
import {Ownable} from "solady/auth/Ownable.sol"; | ||
|
||
import {ABIResolver} from "./resolver/ABIResolver.sol"; | ||
import {AddrResolver} from "./resolver/AddrResolver.sol"; | ||
import {ContentHashResolver} from "./resolver/ContentHashResolver.sol"; | ||
import {DNSResolver} from "./resolver/DNSResolver.sol"; | ||
import {InterfaceResolver} from "./resolver/InterfaceResolver.sol"; | ||
import {NameResolver} from "./resolver/NameResolver.sol"; | ||
import {PubkeyResolver} from "./resolver/PubkeyResolver.sol"; | ||
import {TextResolver} from "./resolver/TextResolver.sol"; | ||
import {IReverseRegistrar} from "src/L2/interface/IReverseRegistrar.sol"; | ||
|
||
/// @title Upgradeable L2 Resolver | ||
/// | ||
/// @notice The upgradeable public resolver for Basenames. 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/basenames) | ||
contract UpgradeableL2Resolver is | ||
ABIResolver, | ||
AddrResolver, | ||
ContentHashResolver, | ||
DNSResolver, | ||
ExtendedResolver, | ||
Initializable, | ||
InterfaceResolver, | ||
Multicallable, | ||
NameResolver, | ||
Ownable, | ||
PubkeyResolver, | ||
TextResolver | ||
{ | ||
/// @notice EIP-7201 storage location. | ||
// keccak256(abi.encode(uint256(keccak256("resolver.storage")) - 1)) & ~bytes32(uint256(0xff)); | ||
bytes32 private constant RESOLVER_STORAGE_LOCATION = | ||
0xa75da70a48b778f6d7794a48ad897d5e41dff6abea13a6164e9a58efe57a7200; | ||
|
||
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | ||
/* STORAGE */ | ||
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | ||
|
||
struct ResolverStorage { | ||
/// @notice The registry contract. | ||
ENS registry; | ||
/// @notice The trusted registrar controller contract. | ||
address registrarController; | ||
/// @notice The reverse registrar contract. | ||
address reverseRegistrar; | ||
/// @notice A mapping of operators per owner address. An operator is authorized to make changes to | ||
/// all names owned by the `owner`. | ||
mapping(address owner => mapping(address operator => bool isApproved)) _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))) _tokenApprovals; | ||
} | ||
|
||
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | ||
/* ERRORS */ | ||
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | ||
|
||
/// @notice Thrown 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 */ | ||
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | ||
|
||
/// @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); | ||
|
||
/// @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 */ | ||
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | ||
|
||
constructor() { | ||
_disableInitializers(); | ||
} | ||
|
||
/// @notice L2 Resolver constructor used to establish the necessary contract configuration. | ||
/// | ||
/// @param registry_ 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. | ||
function initialize(ENS registry_, address registrarController_, address reverseRegistrar_, address owner_) | ||
public | ||
initializer | ||
{ | ||
ResolverStorage storage $ = _getResolverStorage(); | ||
$.registry = registry_; | ||
$.registrarController = registrarController_; | ||
$.reverseRegistrar = reverseRegistrar_; | ||
_initializeOwner(owner_); | ||
IReverseRegistrar(reverseRegistrar_).claim(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 { | ||
_getResolverStorage().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 { | ||
_getResolverStorage().reverseRegistrar = reverseRegistrar_; | ||
emit ReverseRegistrarUpdated(reverseRegistrar_); | ||
} | ||
|
||
/// @dev See {IERC1155-setApprovalForAll}. | ||
function setApprovalForAll(address operator, bool approved) external { | ||
if (msg.sender == operator) revert CantSetSelfAsOperator(); | ||
|
||
_getResolverStorage()._operatorApprovals[msg.sender][operator] = approved; | ||
emit ApprovalForAll(msg.sender, operator, approved); | ||
} | ||
|
||
/// @dev See {IERC1155-isApprovedForAll}. | ||
function isApprovedForAll(address account, address operator) public view returns (bool) { | ||
return _getResolverStorage()._operatorApprovals[account][operator]; | ||
} | ||
|
||
/// @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(); | ||
|
||
_getResolverStorage()._tokenApprovals[msg.sender][node][delegate] = approved; | ||
emit Approved(msg.sender, node, delegate, approved); | ||
} | ||
|
||
/// @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 _getResolverStorage()._tokenApprovals[owner][node][delegate]; | ||
} | ||
|
||
/// @notice Check to see whether `msg.sender` is authorized 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) { | ||
ResolverStorage storage $ = _getResolverStorage(); | ||
if (msg.sender == $.registrarController || msg.sender == $.reverseRegistrar) { | ||
return true; | ||
} | ||
address owner = $.registry.owner(node); | ||
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 | ||
override( | ||
Multicallable, | ||
ABIResolver, | ||
AddrResolver, | ||
ContentHashResolver, | ||
DNSResolver, | ||
InterfaceResolver, | ||
NameResolver, | ||
PubkeyResolver, | ||
TextResolver | ||
) | ||
returns (bool) | ||
{ | ||
return (interfaceID == type(IExtendedResolver).interfaceId || super.supportsInterface(interfaceID)); | ||
} | ||
|
||
/// @notice Returns the address of the trusted registrar controller contract. | ||
function registrarController() external view returns (address) { | ||
return _getResolverStorage().registrarController; | ||
} | ||
|
||
/// @notice Returns the address of the reverse registrar contract. | ||
function reverseRegistrar() external view returns (address) { | ||
return _getResolverStorage().reverseRegistrar; | ||
} | ||
|
||
/// @notice EIP-7201 storage pointer fetch helper. | ||
function _getResolverStorage() internal pure returns (ResolverStorage storage $) { | ||
assembly { | ||
$.slot := RESOLVER_STORAGE_LOCATION | ||
} | ||
} | ||
|
||
} |
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,76 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.23; | ||
|
||
import {IABIResolver} from "ens-contracts/resolvers/profiles/IABIResolver.sol"; | ||
|
||
import {ResolverBase} from "./ResolverBase.sol"; | ||
|
||
/// @title ABI Resolver | ||
/// | ||
/// @notice ENSIP-4 compliant ABI Resolver. Adaptation of the ENS ABIResolver.sol profile contract, with | ||
/// EIP-7201 storage compliance. | ||
/// https://github.com/ensdomains/ens-contracts/blob/staging/contracts/resolvers/profiles/ABIResolver.sol | ||
/// | ||
/// @author Coinbase (https://github.com/base-org/basenames) | ||
abstract contract ABIResolver is IABIResolver, ResolverBase { | ||
struct ABIResolverStorage { | ||
/// @notice ABI record (`bytes`) by content type, node, and version. | ||
mapping(uint64 version => mapping(bytes32 node => mapping(uint256 contentType => bytes data))) versionable_abis; | ||
} | ||
|
||
/// @notice Thrown when setting an ABI with an invalid content type. | ||
error InvalidContentType(); | ||
|
||
/// @notice EIP-7201 storage location. | ||
/// keccak256(abi.encode(uint256(keccak256("abi.resolver.storage")) - 1)) & ~bytes32(uint256(0xff)); | ||
bytes32 private constant ABI_RESOLVER_STORAGE = 0x76dc89e1c49d3cda8f11a131d381f3dbd0df1919a4e1a669330a2763d2821400; | ||
|
||
/// @notice Sets the ABI associated with an ENS node. | ||
/// | ||
/// @dev Nodes may have one ABI of each content type. To remove an ABI, set it to | ||
/// the empty string. | ||
/// | ||
/// @param node The node to update. | ||
/// @param contentType The content type of the ABI. | ||
/// @param data The ABI data. | ||
function setABI(bytes32 node, uint256 contentType, bytes calldata data) external virtual authorised(node) { | ||
// Content types must be powers of 2 | ||
if (((contentType - 1) & contentType) != 0) revert InvalidContentType(); | ||
|
||
_getABIResolverStorage().versionable_abis[_getResolverBaseStorage().recordVersions[node]][node][contentType] = | ||
data; | ||
emit ABIChanged(node, contentType); | ||
} | ||
|
||
/// @notice Returns the ABI associated with an ENS node for a specific content type. | ||
/// | ||
/// @param node The ENS node to query | ||
/// @param contentTypes A bitwise OR of the ABI formats accepted by the caller. | ||
/// | ||
/// @return contentType The content type of the return value | ||
/// @return data The ABI data | ||
function ABI(bytes32 node, uint256 contentTypes) external view virtual override returns (uint256, bytes memory) { | ||
mapping(uint256 => bytes) storage abiset = | ||
_getABIResolverStorage().versionable_abis[_getResolverBaseStorage().recordVersions[node]][node]; | ||
|
||
for (uint256 contentType = 1; contentType <= contentTypes; contentType <<= 1) { | ||
if ((contentType & contentTypes) != 0 && abiset[contentType].length > 0) { | ||
return (contentType, abiset[contentType]); | ||
} | ||
} | ||
|
||
return (0, bytes("")); | ||
} | ||
|
||
/// @notice ERC-165 compliance. | ||
function supportsInterface(bytes4 interfaceID) public view virtual override returns (bool) { | ||
return interfaceID == type(IABIResolver).interfaceId || super.supportsInterface(interfaceID); | ||
} | ||
|
||
/// @notice EIP-7201 storage pointer fetch helper. | ||
function _getABIResolverStorage() internal pure returns (ABIResolverStorage storage $) { | ||
assembly { | ||
$.slot := ABI_RESOLVER_STORAGE | ||
} | ||
} | ||
} |
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.