-
Notifications
You must be signed in to change notification settings - Fork 37
/
Copy pathL1Resolver.sol
257 lines (226 loc) · 12.3 KB
/
L1Resolver.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
import {IExtendedResolver} from "ens-contracts/resolvers/profiles/IExtendedResolver.sol";
import {ERC165} from "lib/openzeppelin-contracts/contracts/utils/introspection/ERC165.sol";
import {Ownable} from "solady/auth/Ownable.sol";
import {BASE_ETH_NAME} from "src/util/Constants.sol";
import {SignatureVerifier} from "src/lib/SignatureVerifier.sol";
/// @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 signer => bool isApproved) 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 AddedSigners(address[] signers);
/// @notice Emitted when a new gateway url was stored for `url`.
///
/// @param newUrl the new url being stored.
event UrlChanged(string newUrl);
/// @notice Emitted when a new root resolver is set as the `rootResolver`.
///
/// @param resolver The address of the new root resolver.
event RootResolverChanged(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 `AddedSigners(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_);
rootResolver = rootResolver_;
for (uint256 i = 0; i < signers_.length; i++) {
signers[signers_[i]] = true;
}
emit AddedSigners(signers_);
}
/// @notice Permissioned method letting the owner set the gateway url.
///
/// @dev Emits `UrlChanged(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 UrlChanged(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 AddedSigners(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 `RootResolverChanged(rootResolver_)` after setting the `rootResolver` address.
///
/// @param rootResolver_ Address of the new `rootResolver`
function setRootResolver(address rootResolver_) external onlyOwner {
rootResolver = rootResolver_;
emit RootResolverChanged(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
returns (bytes32)
{
return SignatureVerifier.makeSignatureHash(target, expires, request, result);
}
/// @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) {
// Check for base.eth resolution, and resolve return early if so
if (keccak256(BASE_ETH_NAME) == keccak256(name)) {
return _resolve(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);
}
/// @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 response 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.
///
/// @dev Checks interface support for this contract OR ERC165 OR rootResolver
/// 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 returns (bool) {
return interfaceID == type(IExtendedResolver).interfaceId || super.supportsInterface(interfaceID)
|| ERC165(rootResolver).supportsInterface(interfaceID);
}
/// @notice Internal method for completing `resolve` intended for the `rootResolver`.
///
/// @dev The `PublicResolver` located at `rootResolver` does not implement the `resolve(bytes,bytes)` method.
/// This method completes the resolution request by staticcalling `rootResolver` with the resolve request.
/// Implementation matches the ENS `ExtendedResolver:resolve(bytes,bytes)` method with the exception that it `staticcall`s the
/// the `rootResolver` instead of `address(this)`.
///
/// @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 memory, bytes memory data) internal view returns (bytes memory) {
(bool success, bytes memory result) = rootResolver.staticcall(data);
if (success) {
return result;
} else {
// Revert with the reason provided by the call
assembly {
revert(add(result, 0x20), mload(result))
}
}
}
/// @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 {
// Copy msg.data. We take full control of memory in this inline assembly
// block because it will not return to Solidity code. We overwrite the
// Solidity scratch pad at memory position 0.
calldatacopy(0, 0, calldatasize())
// Call the root resolver.
// out and outsize are 0 because we don't know the size yet.
let result := call(gas(), RESOLVER, 0, 0, calldatasize(), 0, 0)
// Copy the returned data.
returndatacopy(0, 0, returndatasize())
switch result
// call returns 0 on error.
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}