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

feat: Allow ENS resolver to answer more queries #464

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ BUNDLER_OWNER_ADDRESS=0x53c6dA835c777AD11159198FBe11f95E5eE6B692
RECOVERY_PROXY_OWNER_ADDRESS=0xFFE52568Fb0E7038Ef289677288BB704E5c9E82e

# Fname resolver params.
FNAME_RESOLVER_DNS_ENCODED_NAME=0x096661726361737465720365746800
FNAME_RESOLVER_PASSTHROUGH_ADDRESS=0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63
FNAME_RESOLVER_SERVER_URL=https://fnames.farcaster.xyz/ccip/{sender}/{data}.json
FNAME_RESOLVER_SIGNER_ADDRESS=0xBc5274eFc266311015793d89E9B591fa46294741
FNAME_RESOLVER_OWNER_ADDRESS=0x138356f24c7A16BE48978dE277a468F6C16A19a5
Expand Down
2 changes: 2 additions & 0 deletions .env.local
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ RECOVERY_PROXY_OWNER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

# Fname resolver params.
# Default owner is Anvil test account 1
FNAME_RESOLVER_DNS_ENCODED_NAME=0x096661726361737465720365746800
FNAME_RESOLVER_PASSTHROUGH_ADDRESS=0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63
FNAME_RESOLVER_SERVER_URL=https://fnames.farcaster.xyz/ccip/{sender}/{data}.json
FNAME_RESOLVER_SIGNER_ADDRESS=0xBc5274eFc266311015793d89E9B591fa46294741
FNAME_RESOLVER_OWNER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Expand Down
2 changes: 2 additions & 0 deletions .env.prod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ BUNDLER_OWNER_ADDRESS=0x53c6dA835c777AD11159198FBe11f95E5eE6B692
RECOVERY_PROXY_OWNER_ADDRESS=0xFFE52568Fb0E7038Ef289677288BB704E5c9E82e

# Fname resolver params.
FNAME_RESOLVER_DNS_ENCODED_NAME=0x096661726361737465720365746800
FNAME_RESOLVER_PASSTHROUGH_ADDRESS=0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63
FNAME_RESOLVER_SERVER_URL=https://fnames.farcaster.xyz/ccip/{sender}/{data}.json
FNAME_RESOLVER_SIGNER_ADDRESS=0xBc5274eFc266311015793d89E9B591fa46294741
FNAME_RESOLVER_OWNER_ADDRESS=0x138356f24c7A16BE48978dE277a468F6C16A19a5
Expand Down
2 changes: 2 additions & 0 deletions .env.upgrade
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ BUNDLER_OWNER_ADDRESS=
RECOVERY_PROXY_OWNER_ADDRESS=0xFFE52568Fb0E7038Ef289677288BB704E5c9E82e

# Fname resolver params.
FNAME_RESOLVER_DNS_ENCODED_NAME=0x096661726361737465720365746800
FNAME_RESOLVER_PASSTHROUGH_ADDRESS=0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63
FNAME_RESOLVER_SERVER_URL=https://fnames.farcaster.xyz/ccip/{sender}/{data}.json
FNAME_RESOLVER_SIGNER_ADDRESS=0xBc5274eFc266311015793d89E9B591fa46294741
FNAME_RESOLVER_OWNER_ADDRESS=0x138356f24c7A16BE48978dE277a468F6C16A19a5
Expand Down
2 changes: 1 addition & 1 deletion .rusty-hook.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[hooks]
pre-commit = "forge fmt --check && forge snapshot --check --match-contract Gas && forge test -vvv"
pre-commit = "forge snapshot --check --match-contract Gas && forge test -vvv"

[logging]
verbose = true
10 changes: 8 additions & 2 deletions script/DeployL1.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ contract DeployL1 is ImmutableCreate2Deployer {
bytes32 internal constant FNAME_RESOLVER_CREATE2_SALT = bytes32(0);

struct DeploymentParams {
bytes name;
address resolver;
string serverURL;
address signer;
address owner;
Expand All @@ -22,7 +24,9 @@ contract DeployL1 is ImmutableCreate2Deployer {
runDeploy(loadDeploymentParams());
}

function runDeploy(DeploymentParams memory params) public returns (Contracts memory) {
function runDeploy(
DeploymentParams memory params
) public returns (Contracts memory) {
return runDeploy(params, true);
}

Expand All @@ -31,7 +35,7 @@ contract DeployL1 is ImmutableCreate2Deployer {
"FnameResolver",
FNAME_RESOLVER_CREATE2_SALT,
type(FnameResolver).creationCode,
abi.encode(params.serverURL, params.signer, params.owner)
abi.encode(params.name, params.resolver, params.serverURL, params.signer, params.owner)
);

deploy(broadcast);
Expand All @@ -41,6 +45,8 @@ contract DeployL1 is ImmutableCreate2Deployer {

function loadDeploymentParams() internal view returns (DeploymentParams memory) {
return DeploymentParams({
name: vm.envBytes("FNAME_RESOLVER_DNS_ENCODED_NAME"),
resolver: vm.envAddress("FNAME_RESOLVER_PASSTHROUGH_ADDRESS"),
serverURL: vm.envString("FNAME_RESOLVER_SERVER_URL"),
signer: vm.envAddress("FNAME_RESOLVER_SIGNER_ADDRESS"),
owner: vm.envAddress("FNAME_RESOLVER_OWNER_ADDRESS"),
Expand Down
98 changes: 76 additions & 22 deletions src/FnameResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ import {ERC165} from "openzeppelin/contracts/utils/introspection/ERC165.sol";

import {EIP712} from "./abstract/EIP712.sol";

interface IAddressQuery {
function addr(bytes32 node) external view returns (address);
}

interface IExtendedResolver {
function resolve(bytes memory name, bytes memory data) external view returns (bytes memory);
}
Expand All @@ -19,7 +15,7 @@ interface IResolverService {
function resolve(
bytes calldata name,
bytes calldata data
) external view returns (string memory fname, uint256 timestamp, address owner, bytes memory signature);
) external view returns (bytes memory result, uint256 validUntil, address owner, bytes memory signature);
}

/**
Expand Down Expand Up @@ -51,6 +47,12 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step {
/// @dev Revert if the recovered signer address is not an authorized signer.
error InvalidSigner();

/// @dev Revert if the extra data hash does not match the original request.
error MismatchedRequest();

/// @dev Revert if the signature is expired.
error ExpiredSignature();

/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -79,10 +81,10 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step {
string public constant VERSION = "2023.08.23";

/**
* @dev EIP-712 typehash of the UsernameProof struct.
* @dev EIP-712 typehash of the DataProof struct.
*/
bytes32 public constant USERNAME_PROOF_TYPEHASH =
keccak256("UserNameProof(string name,uint256 timestamp,address owner)");
bytes32 public constant DATA_PROOF_TYPEHASH =
keccak256("DataProof(bytes32 request,bytes32 result,uint256 validUntil)");

/*//////////////////////////////////////////////////////////////
PARAMETERS
Expand All @@ -93,31 +95,67 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step {
*/
string public url;

/**
* @dev DNS-encoded name this contract will be used with.
*/
bytes public dnsEncodedName;

/**
* @dev Address of the ENS resolver to passthrough calls to `name` to.
*/
IExtendedResolver public immutable passthroughResolver;

/**
* @dev Mapping of signer address to authorized boolean.
*/
mapping(address signer => bool isAuthorized) public signers;

/**
* @dev Mapping of resolver function selector to allowed boolean.
*/
mapping(bytes4 selector => bool isAllowed) public allowedSelectors;

/**
* @dev Mapping of text record key to allowed boolean.
*/
mapping(string textRecordKey => bool isAllowed) public allowedTextRecords;

/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/

/**
* @notice Set the lookup gateway URL and initial signer.
*
* @param _name DNS-encoded name this contract will be used with. This is set permanently.
* @param _resolver Address of the ENS resolver to passthrough calls to the 2LD to. This is set permanently.
* @param _url Lookup gateway URL. This value is set permanently.
* @param _signer Initial authorized signer address.
* @param _initialOwner Initial owner address.
*/
constructor(
bytes memory _name,
IExtendedResolver _resolver,
string memory _url,
address _signer,
address _initialOwner
) EIP712("Farcaster name verification", "1") {
_transferOwnership(_initialOwner);
dnsEncodedName = _name;
passthroughResolver = _resolver;
url = _url;
signers[_signer] = true;
emit AddSigner(_signer);

// Only support `addr(node)`, `addr(node, cointype)` and `text(node, key)`
allowedSelectors[0x3b3b57de] = true;
allowedSelectors[0xf1cb7e06] = true;
allowedSelectors[0x59d1d43c] = true;

// Only support `avatar`, `description` and `url`
allowedTextRecords["avatar"] = true;
allowedTextRecords["description"] = true;
allowedTextRecords["url"] = true;
}

/*//////////////////////////////////////////////////////////////
Expand All @@ -134,10 +172,20 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step {
* other resolver function will revert.
*/
function resolve(bytes calldata name, bytes calldata data) external view returns (bytes memory) {
if (bytes4(data[:4]) != IAddressQuery.addr.selector) {
if (keccak256(name) == keccak256(dnsEncodedName)) {
return passthroughResolver.resolve(name, data);
}

if (!allowedSelectors[bytes4(data[:4])]) {
revert ResolverFunctionNotSupported();
}

// Save requests to the gateway by only forwarding certain text record lookups
if (bytes4(data[:4]) == 0x59d1d43c) {
(, string memory key) = abi.decode(data[4:], (bytes32, string));
if (!allowedTextRecords[key]) return abi.encode("");
}

bytes memory callData = abi.encodeCall(IResolverService.resolve, (name, data));
string[] memory urls = new string[](1);
urls[0] = url;
Expand All @@ -154,24 +202,24 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step {
* - uint256: Timestamp of the username proof.
* - address: Owner address that signed the username proof.
* - bytes: EIP-712 signature provided by the CCIP gateway server.
* @param extraData Calldata from the original resolve() call. Used to verify that the gateway is answering the
* right query.
*
* @return ABI-encoded address of the fname owner.
* @return ABI-encoded data (can be address or text record).
*/
function resolveWithProof(
bytes calldata response,
bytes calldata /* extraData */
) external view returns (bytes memory) {
(string memory fname, uint256 timestamp, address fnameOwner, bytes memory signature) =
abi.decode(response, (string, uint256, address, bytes));
function resolveWithProof(bytes calldata response, bytes calldata extraData) external view returns (bytes memory) {
(bytes32 extraDataHash, bytes memory result, uint256 validUntil, bytes memory signature) =
abi.decode(response, (bytes32, bytes, uint256, bytes));

bytes32 proofHash =
keccak256(abi.encode(USERNAME_PROOF_TYPEHASH, keccak256(bytes(fname)), timestamp, fnameOwner));
bytes32 proofHash = keccak256(abi.encode(DATA_PROOF_TYPEHASH, extraDataHash, keccak256(result), validUntil));
bytes32 eip712hash = _hashTypedDataV4(proofHash);
address signer = ECDSA.recover(eip712hash, signature);

if (!signers[signer]) revert InvalidSigner();
if (block.timestamp > validUntil) revert ExpiredSignature();
if (keccak256(extraData) != extraDataHash) revert MismatchedRequest();

return abi.encode(fnameOwner);
return result;
}

/*//////////////////////////////////////////////////////////////
Expand All @@ -183,7 +231,9 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step {
*
* @param signer The signer address.
*/
function addSigner(address signer) external onlyOwner {
function addSigner(
address signer
) external onlyOwner {
signers[signer] = true;
emit AddSigner(signer);
}
Expand All @@ -193,7 +243,9 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step {
*
* @param signer The signer address.
*/
function removeSigner(address signer) external onlyOwner {
function removeSigner(
address signer
) external onlyOwner {
signers[signer] = false;
emit RemoveSigner(signer);
}
Expand All @@ -202,7 +254,9 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step {
INTERFACE DETECTION
//////////////////////////////////////////////////////////////*/

function supportsInterface(bytes4 interfaceId) public view override returns (bool) {
function supportsInterface(
bytes4 interfaceId
) public view override returns (bool) {
return interfaceId == type(IExtendedResolver).interfaceId || super.supportsInterface(interfaceId);
}
}
16 changes: 11 additions & 5 deletions test/Deploy/DeployL1.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import "forge-std/console.sol";
contract DeployL1Test is DeployL1, FnameResolverTestSuite {
address internal deployer = address(this);
address internal alpha = makeAddr("alpha");
address internal alice = makeAddr("alice");

function setUp() public override {
vm.createSelectFork("l1_mainnet");

(signer, signerPk) = makeAddrAndKey("signer");

DeployL1.DeploymentParams memory params = DeployL1.DeploymentParams({
name: hex"096661726361737465720365746800",
resolver: 0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63,
serverURL: "https://fnames.farcaster.xyz/ccip/{sender}/{data}.json",
signer: signer,
owner: alpha,
Expand All @@ -33,17 +34,22 @@ contract DeployL1Test is DeployL1, FnameResolverTestSuite {

function test_deploymentParams() public {
// Check deployment parameters
assertEq(resolver.dnsEncodedName(), hex"096661726361737465720365746800");
assertEq(address(resolver.passthroughResolver()), 0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63);
assertEq(resolver.url(), "https://fnames.farcaster.xyz/ccip/{sender}/{data}.json");
assertEq(resolver.owner(), alpha);
assertEq(resolver.signers(signer), true);
}

function test_e2e() public {
uint256 timestamp = block.timestamp - 60;
bytes memory signature = _signProof(signerPk, "alice.fcast.id", timestamp, alice);
// calldata of resolve()
bytes memory extraData = abi.encodeCall(IResolverService.resolve, (DNS_ENCODED_NAME, ADDR_QUERY_CALLDATA));
bytes32 requestHash = keccak256(extraData);
uint256 expiration = block.timestamp + 60;
bytes memory result = abi.encode(address(alpha));
bytes memory signature = _signProof(signerPk, requestHash, result, expiration);
bytes memory response =
resolver.resolveWithProof(abi.encode("alice.fcast.id", timestamp, alice, signature), extraData);
assertEq(response, abi.encode(alice));
resolver.resolveWithProof(abi.encode(requestHash, result, expiration, signature), extraData);
assertEq(response, result);
}
}
Loading