Skip to content
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

Natspec #24

Merged
merged 12 commits into from
Jun 14, 2024
140 changes: 119 additions & 21 deletions src/L1/L1Resolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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_);
Expand All @@ -36,30 +86,58 @@ 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;
}
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];
emit RemovedSigner(signer);
}
}

/// @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
Expand All @@ -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
///
Expand All @@ -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 {
Expand All @@ -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()) }
}
Expand Down
Loading