From 7c2110d719fe3d2483911853e6567fa793df080a Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Sat, 8 Feb 2025 03:27:00 -0500 Subject: [PATCH 1/6] Update resolver to handle multiple record types --- src/FnameResolver.sol | 36 ++++++------ test/FnameResolver/FnameResolver.t.sol | 58 ++++++++++--------- test/FnameResolver/FnameResolverTestSuite.sol | 8 +-- 3 files changed, 54 insertions(+), 48 deletions(-) diff --git a/src/FnameResolver.sol b/src/FnameResolver.sol index d9b6b039..75aedb35 100644 --- a/src/FnameResolver.sol +++ b/src/FnameResolver.sol @@ -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); } @@ -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 timestamp, address owner, bytes memory signature); } /** @@ -79,10 +75,9 @@ 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(bytes data,uint256 timestamp,address owner)"); /*////////////////////////////////////////////////////////////// PARAMETERS @@ -98,6 +93,11 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step { */ mapping(address signer => bool isAuthorized) public signers; + /** + * @dev Mapping of resolver function selector to allowed boolean. + */ + mapping(bytes4 selector => bool isAllowed) public allowedSelectors; + /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -118,6 +118,11 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step { 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; } /*////////////////////////////////////////////////////////////// @@ -134,9 +139,7 @@ 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) { - revert ResolverFunctionNotSupported(); - } + if (!allowedSelectors[bytes4(data[:4])]) revert ResolverFunctionNotSupported(); bytes memory callData = abi.encodeCall(IResolverService.resolve, (name, data)); string[] memory urls = new string[](1); @@ -155,23 +158,22 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step { * - address: Owner address that signed the username proof. * - bytes: EIP-712 signature provided by the CCIP gateway server. * - * @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)); + (bytes memory result, uint256 timestamp, address fnameOwner, bytes memory signature) = + abi.decode(response, (bytes, uint256, address, bytes)); - bytes32 proofHash = - keccak256(abi.encode(USERNAME_PROOF_TYPEHASH, keccak256(bytes(fname)), timestamp, fnameOwner)); + bytes32 proofHash = keccak256(abi.encode(DATA_PROOF_TYPEHASH, keccak256(result), timestamp, fnameOwner)); bytes32 eip712hash = _hashTypedDataV4(proofHash); address signer = ECDSA.recover(eip712hash, signature); if (!signers[signer]) revert InvalidSigner(); - return abi.encode(fnameOwner); + return result; } /*////////////////////////////////////////////////////////////// diff --git a/test/FnameResolver/FnameResolver.t.sol b/test/FnameResolver/FnameResolver.t.sol index 52d11ba9..3852fc1a 100644 --- a/test/FnameResolver/FnameResolver.t.sol +++ b/test/FnameResolver/FnameResolver.t.sol @@ -5,7 +5,7 @@ import "forge-std/Test.sol"; import {IERC165} from "openzeppelin/contracts/utils/introspection/IERC165.sol"; import {FnameResolverTestSuite} from "./FnameResolverTestSuite.sol"; -import {FnameResolver, IResolverService, IExtendedResolver, IAddressQuery} from "../../src/FnameResolver.sol"; +import {FnameResolver, IResolverService, IExtendedResolver} from "../../src/FnameResolver.sol"; /* solhint-disable state-visibility */ @@ -34,7 +34,7 @@ contract FnameResolverTest is FnameResolverTestSuite { //////////////////////////////////////////////////////////////*/ function testFuzzResolveRevertsWithOffchainLookup(bytes calldata name, bytes memory data) public { - data = bytes.concat(IAddressQuery.addr.selector, data); + data = bytes.concat(bytes4(0x3b3b57de), data); string[] memory urls = new string[](1); urls[0] = FNAME_SERVER_URL; @@ -64,46 +64,46 @@ contract FnameResolverTest is FnameResolverTestSuite { RESOLVE WITH PROOF //////////////////////////////////////////////////////////////*/ - function testFuzzResolveWithProofValidSignature(string memory name, uint256 timestamp, address owner) public { - bytes memory signature = _signProof(name, timestamp, owner); + function testFuzzResolveWithProofValidSignature(bytes memory result, uint256 timestamp, address owner) public { + bytes memory signature = _signProof(result, timestamp, owner); bytes memory extraData = abi.encodeCall(IResolverService.resolve, (DNS_ENCODED_NAME, ADDR_QUERY_CALLDATA)); - bytes memory response = resolver.resolveWithProof(abi.encode(name, timestamp, owner, signature), extraData); - assertEq(response, abi.encode(owner)); + bytes memory response = resolver.resolveWithProof(abi.encode(result, timestamp, owner, signature), extraData); + assertEq(response, result); } - function testFuzzResolveWithProofInvalidOwner(string memory name, uint256 timestamp, address owner) public { + function testFuzzResolveWithProofInvalidOwner(bytes memory result, uint256 timestamp, address owner) public { address wrongOwner = address(~uint160(owner)); - bytes memory signature = _signProof(name, timestamp, owner); + bytes memory signature = _signProof(result, timestamp, owner); vm.expectRevert(FnameResolver.InvalidSigner.selector); - resolver.resolveWithProof(abi.encode(name, timestamp, wrongOwner, signature), ""); + resolver.resolveWithProof(abi.encode(result, timestamp, wrongOwner, signature), ""); } - function testFuzzResolveWithProofInvalidTimestamp(string memory name, uint256 timestamp, address owner) public { + function testFuzzResolveWithProofInvalidTimestamp(bytes memory result, uint256 timestamp, address owner) public { uint256 wrongTimestamp = ~timestamp; - bytes memory signature = _signProof(name, timestamp, owner); + bytes memory signature = _signProof(result, timestamp, owner); vm.expectRevert(FnameResolver.InvalidSigner.selector); - resolver.resolveWithProof(abi.encode(name, wrongTimestamp, owner, signature), ""); + resolver.resolveWithProof(abi.encode(result, wrongTimestamp, owner, signature), ""); } - function testFuzzResolveWithProofInvalidName(string memory name, uint256 timestamp, address owner) public { - string memory wrongName = string.concat("~", name); - bytes memory signature = _signProof(name, timestamp, owner); + function testFuzzResolveWithProofInvalidName(bytes memory result, uint256 timestamp, address owner) public { + bytes memory wrongResult = bytes.concat(bytes4(0x00000001), result); + bytes memory signature = _signProof(result, timestamp, owner); vm.expectRevert(FnameResolver.InvalidSigner.selector); - resolver.resolveWithProof(abi.encode(wrongName, timestamp, owner, signature), ""); + resolver.resolveWithProof(abi.encode(wrongResult, timestamp, owner, signature), ""); } - function testFuzzResolveWithProofWrongSigner(string memory name, uint256 timestamp, address owner) public { - bytes memory signature = _signProof(malloryPk, name, timestamp, owner); + function testFuzzResolveWithProofWrongSigner(bytes memory result, uint256 timestamp, address owner) public { + bytes memory signature = _signProof(malloryPk, result, timestamp, owner); vm.expectRevert(FnameResolver.InvalidSigner.selector); - resolver.resolveWithProof(abi.encode(name, timestamp, owner, signature), ""); + resolver.resolveWithProof(abi.encode(result, timestamp, owner, signature), ""); } function testFuzzResolveWithProofInvalidSignerLength( - string memory name, + bytes memory result, uint256 timestamp, address owner, bytes memory signature, @@ -116,20 +116,20 @@ contract FnameResolverTest is FnameResolverTestSuite { } /* truncate signature length */ vm.expectRevert("ECDSA: invalid signature length"); - resolver.resolveWithProof(abi.encode(name, timestamp, owner, signature), ""); + resolver.resolveWithProof(abi.encode(result, timestamp, owner, signature), ""); } function testProofTypehash() public { - assertEq( - resolver.USERNAME_PROOF_TYPEHASH(), keccak256("UserNameProof(string name,uint256 timestamp,address owner)") - ); + assertEq(resolver.DATA_PROOF_TYPEHASH(), keccak256("DataProof(bytes data,uint256 timestamp,address owner)")); } /*////////////////////////////////////////////////////////////// SIGNERS //////////////////////////////////////////////////////////////*/ - function testFuzzOwnerCanAddSigner(address signer) public { + function testFuzzOwnerCanAddSigner( + address signer + ) public { vm.expectEmit(true, false, false, false); emit AddSigner(signer); @@ -147,7 +147,9 @@ contract FnameResolverTest is FnameResolverTestSuite { resolver.addSigner(signer); } - function testFuzzOwnerCanRemoveSigner(address signer) public { + function testFuzzOwnerCanRemoveSigner( + address signer + ) public { vm.prank(owner); resolver.addSigner(signer); @@ -182,7 +184,9 @@ contract FnameResolverTest is FnameResolverTestSuite { assertEq(resolver.supportsInterface(type(IERC165).interfaceId), true); } - function testFuzzInterfaceDetectionUnsupportedInterface(bytes4 interfaceId) public { + function testFuzzInterfaceDetectionUnsupportedInterface( + bytes4 interfaceId + ) public { vm.assume(interfaceId != type(IExtendedResolver).interfaceId && interfaceId != type(IERC165).interfaceId); assertEq(resolver.supportsInterface(interfaceId), false); } diff --git a/test/FnameResolver/FnameResolverTestSuite.sol b/test/FnameResolver/FnameResolverTestSuite.sol index 67f2ed68..f40216df 100644 --- a/test/FnameResolver/FnameResolverTestSuite.sol +++ b/test/FnameResolver/FnameResolverTestSuite.sol @@ -49,21 +49,21 @@ abstract contract FnameResolverTestSuite is TestSuiteSetup { //////////////////////////////////////////////////////////////*/ function _signProof( - string memory name, + bytes memory result, uint256 timestamp, address owner ) internal returns (bytes memory signature) { - return _signProof(signerPk, name, timestamp, owner); + return _signProof(signerPk, result, timestamp, owner); } function _signProof( uint256 pk, - string memory name, + bytes memory result, uint256 timestamp, address owner ) internal returns (bytes memory signature) { bytes32 eip712hash = resolver.hashTypedDataV4( - keccak256(abi.encode(resolver.USERNAME_PROOF_TYPEHASH(), keccak256(bytes(name)), timestamp, owner)) + keccak256(abi.encode(resolver.DATA_PROOF_TYPEHASH(), keccak256(result), timestamp, owner)) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, eip712hash); signature = abi.encodePacked(r, s, v); From 66fe725ddc673eba8238a57043373fa4666ac55f Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Sat, 8 Feb 2025 03:32:03 -0500 Subject: [PATCH 2/6] Add todo --- src/FnameResolver.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/FnameResolver.sol b/src/FnameResolver.sol index 75aedb35..505e049b 100644 --- a/src/FnameResolver.sol +++ b/src/FnameResolver.sol @@ -157,13 +157,13 @@ 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 so the contract can verify that the query the gateway + * answered is the one the contract originally requested. * * @return ABI-encoded data (can be address or text record). */ - function resolveWithProof( - bytes calldata response, - bytes calldata /* extraData */ - ) external view returns (bytes memory) { + // TODO: Add extraData to the proof hash, both in the contract and in the fname server. + function resolveWithProof(bytes calldata response, bytes calldata extraData) external view returns (bytes memory) { (bytes memory result, uint256 timestamp, address fnameOwner, bytes memory signature) = abi.decode(response, (bytes, uint256, address, bytes)); From 0d770a3f060f47b0defdd1fb24e756f04e02ef43 Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Sat, 8 Feb 2025 16:38:41 -0500 Subject: [PATCH 3/6] Verify original request, add sig expiration --- src/FnameResolver.sol | 34 +++++++--- test/Deploy/DeployL1.t.sol | 12 ++-- test/FnameResolver/FnameResolver.t.sol | 65 ++++++++++--------- test/FnameResolver/FnameResolverTestSuite.sol | 24 ++++--- 4 files changed, 79 insertions(+), 56 deletions(-) diff --git a/src/FnameResolver.sol b/src/FnameResolver.sol index 505e049b..753dcdd9 100644 --- a/src/FnameResolver.sol +++ b/src/FnameResolver.sol @@ -47,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 //////////////////////////////////////////////////////////////*/ @@ -77,7 +83,8 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step { /** * @dev EIP-712 typehash of the DataProof struct. */ - bytes32 public constant DATA_PROOF_TYPEHASH = keccak256("DataProof(bytes data,uint256 timestamp,address owner)"); + bytes32 public constant DATA_PROOF_TYPEHASH = + keccak256("DataProof(bytes32 extraDataHash,bytes result,uint256 validUntil)"); /*////////////////////////////////////////////////////////////// PARAMETERS @@ -157,21 +164,22 @@ 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 so the contract can verify that the query the gateway - * answered is the one the contract originally requested. + * @param extraData Calldata from the original resolve() call. Used to verify that the gateway is answering the + * right query. * * @return ABI-encoded data (can be address or text record). */ - // TODO: Add extraData to the proof hash, both in the contract and in the fname server. function resolveWithProof(bytes calldata response, bytes calldata extraData) external view returns (bytes memory) { - (bytes memory result, uint256 timestamp, address fnameOwner, bytes memory signature) = - abi.decode(response, (bytes, uint256, address, bytes)); + (bytes32 extraDataHash, bytes memory result, uint256 validUntil, bytes memory signature) = + abi.decode(response, (bytes32, bytes, uint256, bytes)); - bytes32 proofHash = keccak256(abi.encode(DATA_PROOF_TYPEHASH, keccak256(result), 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 result; } @@ -185,7 +193,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); } @@ -195,7 +205,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); } @@ -204,7 +216,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); } } diff --git a/test/Deploy/DeployL1.t.sol b/test/Deploy/DeployL1.t.sol index ba118d8c..dac9ad48 100644 --- a/test/Deploy/DeployL1.t.sol +++ b/test/Deploy/DeployL1.t.sol @@ -12,7 +12,6 @@ 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"); @@ -39,11 +38,14 @@ contract DeployL1Test is DeployL1, FnameResolverTestSuite { } 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); } } diff --git a/test/FnameResolver/FnameResolver.t.sol b/test/FnameResolver/FnameResolver.t.sol index 3852fc1a..547acb8b 100644 --- a/test/FnameResolver/FnameResolver.t.sol +++ b/test/FnameResolver/FnameResolver.t.sol @@ -51,7 +51,7 @@ contract FnameResolverTest is FnameResolverTestSuite { resolver.resolve(name, data); } - function testFuzzResolveRevertsNonAddrFunction(bytes calldata name, bytes memory data) public { + function testFuzzResolveRevertsUnsupportedFunction(bytes calldata name, bytes memory data) public { data = bytes.concat(hex"00000001", data); string[] memory urls = new string[](1); urls[0] = FNAME_SERVER_URL; @@ -64,48 +64,48 @@ contract FnameResolverTest is FnameResolverTestSuite { RESOLVE WITH PROOF //////////////////////////////////////////////////////////////*/ - function testFuzzResolveWithProofValidSignature(bytes memory result, uint256 timestamp, address owner) public { - bytes memory signature = _signProof(result, timestamp, owner); + function testFuzzResolveWithProofValidSignature(bytes memory result, uint256 validUntil) public { + vm.assume(validUntil > block.timestamp); bytes memory extraData = abi.encodeCall(IResolverService.resolve, (DNS_ENCODED_NAME, ADDR_QUERY_CALLDATA)); - bytes memory response = resolver.resolveWithProof(abi.encode(result, timestamp, owner, signature), extraData); + bytes32 extraDataHash = keccak256(extraData); + bytes memory signature = _signProof(extraDataHash, result, validUntil); + bytes memory response = + resolver.resolveWithProof(abi.encode(extraDataHash, result, validUntil, signature), extraData); assertEq(response, result); } - function testFuzzResolveWithProofInvalidOwner(bytes memory result, uint256 timestamp, address owner) public { - address wrongOwner = address(~uint160(owner)); - bytes memory signature = _signProof(result, timestamp, owner); - - vm.expectRevert(FnameResolver.InvalidSigner.selector); - resolver.resolveWithProof(abi.encode(result, timestamp, wrongOwner, signature), ""); - } - - function testFuzzResolveWithProofInvalidTimestamp(bytes memory result, uint256 timestamp, address owner) public { - uint256 wrongTimestamp = ~timestamp; - bytes memory signature = _signProof(result, timestamp, owner); - - vm.expectRevert(FnameResolver.InvalidSigner.selector); - resolver.resolveWithProof(abi.encode(result, wrongTimestamp, owner, signature), ""); + function testFuzzResolveWithProofMismatchingRequest(bytes memory result, uint256 validUntil) public { + vm.assume(validUntil > block.timestamp); + bytes memory extraData = abi.encodeCall(IResolverService.resolve, (DNS_ENCODED_NAME, ADDR_QUERY_CALLDATA)); + bytes32 extraDataHash = keccak256(extraData); + bytes memory signature = _signProof(extraDataHash, result, validUntil); + vm.expectRevert(FnameResolver.MismatchedRequest.selector); + resolver.resolveWithProof(abi.encode(extraDataHash, result, validUntil, signature), ""); } - function testFuzzResolveWithProofInvalidName(bytes memory result, uint256 timestamp, address owner) public { - bytes memory wrongResult = bytes.concat(bytes4(0x00000001), result); - bytes memory signature = _signProof(result, timestamp, owner); - - vm.expectRevert(FnameResolver.InvalidSigner.selector); - resolver.resolveWithProof(abi.encode(wrongResult, timestamp, owner, signature), ""); + function testFuzzResolveWithProofExpiredSignature(bytes memory result, uint256 validUntil) public { + vm.assume(validUntil < block.timestamp); + bytes memory extraData = abi.encodeCall(IResolverService.resolve, (DNS_ENCODED_NAME, ADDR_QUERY_CALLDATA)); + bytes32 extraDataHash = keccak256(extraData); + bytes memory signature = _signProof(extraDataHash, result, validUntil); + vm.expectRevert(FnameResolver.ExpiredSignature.selector); + resolver.resolveWithProof(abi.encode(extraDataHash, result, validUntil, signature), extraData); } - function testFuzzResolveWithProofWrongSigner(bytes memory result, uint256 timestamp, address owner) public { - bytes memory signature = _signProof(malloryPk, result, timestamp, owner); + function testFuzzResolveWithProofWrongSigner(bytes memory result, uint256 validUntil) public { + vm.assume(validUntil > block.timestamp); + bytes memory extraData = abi.encodeCall(IResolverService.resolve, (DNS_ENCODED_NAME, ADDR_QUERY_CALLDATA)); + bytes32 extraDataHash = keccak256(extraData); + bytes memory signature = _signProof(malloryPk, extraDataHash, result, validUntil); vm.expectRevert(FnameResolver.InvalidSigner.selector); - resolver.resolveWithProof(abi.encode(result, timestamp, owner, signature), ""); + resolver.resolveWithProof(abi.encode(extraDataHash, result, validUntil, signature), extraData); } function testFuzzResolveWithProofInvalidSignerLength( + bytes32 extraDataHash, bytes memory result, - uint256 timestamp, - address owner, + uint256 validUntil, bytes memory signature, uint8 _length ) public { @@ -116,11 +116,14 @@ contract FnameResolverTest is FnameResolverTestSuite { } /* truncate signature length */ vm.expectRevert("ECDSA: invalid signature length"); - resolver.resolveWithProof(abi.encode(result, timestamp, owner, signature), ""); + resolver.resolveWithProof(abi.encode(extraDataHash, result, validUntil, signature), ""); } function testProofTypehash() public { - assertEq(resolver.DATA_PROOF_TYPEHASH(), keccak256("DataProof(bytes data,uint256 timestamp,address owner)")); + assertEq( + resolver.DATA_PROOF_TYPEHASH(), + keccak256("DataProof(bytes32 extraDataHash,bytes result,uint256 validUntil)") + ); } /*////////////////////////////////////////////////////////////// diff --git a/test/FnameResolver/FnameResolverTestSuite.sol b/test/FnameResolver/FnameResolverTestSuite.sol index f40216df..dddc9625 100644 --- a/test/FnameResolver/FnameResolverTestSuite.sol +++ b/test/FnameResolver/FnameResolverTestSuite.sol @@ -23,14 +23,18 @@ abstract contract FnameResolverTestSuite is TestSuiteSetup { * - 2 bytes for the label ("id") * - A null byte terminating the encoded name. */ - bytes internal constant DNS_ENCODED_NAME = - (hex"05" hex"616c696365" hex"05" hex"6663617374" hex"02" hex"6964" hex"00"); + bytes internal constant DNS_ENCODED_NAME = hex"05616c696365096661726361737465720365746800"; + + /** + * @dev Namehash of "alice.farcaster.eth" + */ + bytes internal constant ENS_NODE = hex"e224cf2d7e9641e5b9cde025d9e3db25df5d8789bb7a5c9f4bb28b3e18c2717e"; /** * @dev Encoded calldata for a call to addr(bytes32 node), where node is the ENS - * nameHash encoded value of "alice.fcast.id" + * nameHash encoded value of "alice.farcaster.eth" */ - bytes internal constant ADDR_QUERY_CALLDATA = hex"c30dc5a16498c5b6d46f97ca0c74d092ebbee1290b1c88f6e435dd4fb306ca36"; + bytes internal constant ADDR_QUERY_CALLDATA = hex"e224cf2d7e9641e5b9cde025d9e3db25df5d8789bb7a5c9f4bb28b3e18c2717e"; address internal signer; uint256 internal signerPk; @@ -49,21 +53,21 @@ abstract contract FnameResolverTestSuite is TestSuiteSetup { //////////////////////////////////////////////////////////////*/ function _signProof( + bytes32 requestHash, bytes memory result, - uint256 timestamp, - address owner + uint256 validUntil ) internal returns (bytes memory signature) { - return _signProof(signerPk, result, timestamp, owner); + return _signProof(signerPk, requestHash, result, validUntil); } function _signProof( uint256 pk, + bytes32 requestHash, bytes memory result, - uint256 timestamp, - address owner + uint256 validUntil ) internal returns (bytes memory signature) { bytes32 eip712hash = resolver.hashTypedDataV4( - keccak256(abi.encode(resolver.DATA_PROOF_TYPEHASH(), keccak256(result), timestamp, owner)) + keccak256(abi.encode(resolver.DATA_PROOF_TYPEHASH(), requestHash, keccak256(result), validUntil)) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, eip712hash); signature = abi.encodePacked(r, s, v); From c48357bb15cce3aa51d619e3e9b7b3714e87a0a2 Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Sat, 8 Feb 2025 17:50:06 -0500 Subject: [PATCH 4/6] Update EIP-712 type --- src/FnameResolver.sol | 2 +- test/FnameResolver/FnameResolver.t.sol | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/FnameResolver.sol b/src/FnameResolver.sol index 753dcdd9..db34c2cc 100644 --- a/src/FnameResolver.sol +++ b/src/FnameResolver.sol @@ -84,7 +84,7 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step { * @dev EIP-712 typehash of the DataProof struct. */ bytes32 public constant DATA_PROOF_TYPEHASH = - keccak256("DataProof(bytes32 extraDataHash,bytes result,uint256 validUntil)"); + keccak256("DataProof(bytes32 request,bytes32 result,uint256 validUntil)"); /*////////////////////////////////////////////////////////////// PARAMETERS diff --git a/test/FnameResolver/FnameResolver.t.sol b/test/FnameResolver/FnameResolver.t.sol index 547acb8b..0c7d234b 100644 --- a/test/FnameResolver/FnameResolver.t.sol +++ b/test/FnameResolver/FnameResolver.t.sol @@ -121,8 +121,7 @@ contract FnameResolverTest is FnameResolverTestSuite { function testProofTypehash() public { assertEq( - resolver.DATA_PROOF_TYPEHASH(), - keccak256("DataProof(bytes32 extraDataHash,bytes result,uint256 validUntil)") + resolver.DATA_PROOF_TYPEHASH(), keccak256("DataProof(bytes32 request,bytes32 result,uint256 validUntil)") ); } From 4e2c2cad1703294206678c832fee35d3c87ace28 Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Sat, 8 Feb 2025 19:58:19 -0500 Subject: [PATCH 5/6] Only forward certain text record keys --- src/FnameResolver.sol | 21 +++++++++++++- test/FnameResolver/FnameResolver.t.sol | 40 ++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/FnameResolver.sol b/src/FnameResolver.sol index db34c2cc..d951f79d 100644 --- a/src/FnameResolver.sol +++ b/src/FnameResolver.sol @@ -44,6 +44,9 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step { /// @dev Revert queries for unimplemented resolver functions. error ResolverFunctionNotSupported(); + /// @dev Revert if the text record key is not allowed. + error TextRecordNotSupported(); + /// @dev Revert if the recovered signer address is not an authorized signer. error InvalidSigner(); @@ -105,6 +108,11 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step { */ mapping(bytes4 selector => bool isAllowed) public allowedSelectors; + /** + * @dev Mapping of text record key to allowed boolean. + */ + mapping(string textRecordKey => bool isAllowed) public allowedTextRecords; + /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -126,10 +134,15 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step { signers[_signer] = true; emit AddSigner(_signer); - // Only support addr(node), addr(node, cointype), and text(node, key) + // 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; } /*////////////////////////////////////////////////////////////// @@ -148,6 +161,12 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step { function resolve(bytes calldata name, bytes calldata data) external view returns (bytes memory) { 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]) revert TextRecordNotSupported(); + } + bytes memory callData = abi.encodeCall(IResolverService.resolve, (name, data)); string[] memory urls = new string[](1); urls[0] = url; diff --git a/test/FnameResolver/FnameResolver.t.sol b/test/FnameResolver/FnameResolver.t.sol index 0c7d234b..bdeb555e 100644 --- a/test/FnameResolver/FnameResolver.t.sol +++ b/test/FnameResolver/FnameResolver.t.sol @@ -51,6 +51,33 @@ contract FnameResolverTest is FnameResolverTestSuite { resolver.resolve(name, data); } + function testRevertsWithOffchainLookupForTextRecord() public { + string[] memory keys = new string[](3); + keys[0] = "avatar"; + keys[1] = "description"; + keys[2] = "url"; + + string[] memory urls = new string[](1); + urls[0] = FNAME_SERVER_URL; + + for (uint256 i = 0; i < keys.length; i++) { + bytes memory textCallData = abi.encodeWithSelector(0x59d1d43c, ENS_NODE, keys[i]); + bytes memory callData = abi.encodeCall(resolver.resolve, (DNS_ENCODED_NAME, textCallData)); + + bytes memory offchainLookup = abi.encodeWithSelector( + FnameResolver.OffchainLookup.selector, + address(resolver), + urls, + callData, + resolver.resolveWithProof.selector, + callData + ); + + vm.expectRevert(offchainLookup); + resolver.resolve(DNS_ENCODED_NAME, textCallData); + } + } + function testFuzzResolveRevertsUnsupportedFunction(bytes calldata name, bytes memory data) public { data = bytes.concat(hex"00000001", data); string[] memory urls = new string[](1); @@ -60,6 +87,19 @@ contract FnameResolverTest is FnameResolverTestSuite { resolver.resolve(name, data); } + function testFuzzResolveTextRecordNotSupported( + string memory key + ) public { + // Calldata for the text(node, key) function (signature 0x59d1d43c) + bytes memory textCallData = abi.encodeWithSelector(0x59d1d43c, ENS_NODE, key); + + string[] memory urls = new string[](1); + urls[0] = FNAME_SERVER_URL; + + vm.expectRevert(FnameResolver.TextRecordNotSupported.selector); + resolver.resolve(DNS_ENCODED_NAME, textCallData); + } + /*////////////////////////////////////////////////////////////// RESOLVE WITH PROOF //////////////////////////////////////////////////////////////*/ From 73105434462db9d0c739ccc052ddf5a6815b62b3 Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 11 Feb 2025 01:17:15 -0500 Subject: [PATCH 6/6] Add passthrough, fix unsupported text records --- .env.example | 2 + .env.local | 2 + .env.prod | 2 + .env.upgrade | 2 + .rusty-hook.toml | 2 +- script/DeployL1.s.sol | 10 +++- src/FnameResolver.sol | 31 ++++++++++--- test/Deploy/DeployL1.t.sol | 4 ++ test/FnameResolver/FnameResolver.t.sol | 25 ++++++++-- test/FnameResolver/FnameResolverTestSuite.sol | 46 +++++++++++++++---- 10 files changed, 104 insertions(+), 22 deletions(-) diff --git a/.env.example b/.env.example index bfb2304f..f2311051 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/.env.local b/.env.local index ff67da31..05276935 100644 --- a/.env.local +++ b/.env.local @@ -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 diff --git a/.env.prod b/.env.prod index 9f2138a9..8dad3073 100644 --- a/.env.prod +++ b/.env.prod @@ -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 diff --git a/.env.upgrade b/.env.upgrade index 032bd742..be161f6f 100644 --- a/.env.upgrade +++ b/.env.upgrade @@ -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 diff --git a/.rusty-hook.toml b/.rusty-hook.toml index 398873a1..a7c3c06e 100644 --- a/.rusty-hook.toml +++ b/.rusty-hook.toml @@ -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 diff --git a/script/DeployL1.s.sol b/script/DeployL1.s.sol index 369ffadd..0248409a 100644 --- a/script/DeployL1.s.sol +++ b/script/DeployL1.s.sol @@ -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; @@ -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); } @@ -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); @@ -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"), diff --git a/src/FnameResolver.sol b/src/FnameResolver.sol index d951f79d..a52e4091 100644 --- a/src/FnameResolver.sol +++ b/src/FnameResolver.sol @@ -15,7 +15,7 @@ interface IResolverService { function resolve( bytes calldata name, bytes calldata data - ) external view returns (bytes memory result, uint256 timestamp, address owner, bytes memory signature); + ) external view returns (bytes memory result, uint256 validUntil, address owner, bytes memory signature); } /** @@ -44,9 +44,6 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step { /// @dev Revert queries for unimplemented resolver functions. error ResolverFunctionNotSupported(); - /// @dev Revert if the text record key is not allowed. - error TextRecordNotSupported(); - /// @dev Revert if the recovered signer address is not an authorized signer. error InvalidSigner(); @@ -98,6 +95,16 @@ 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. */ @@ -120,16 +127,22 @@ contract FnameResolver is IExtendedResolver, EIP712, ERC165, Ownable2Step { /** * @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); @@ -159,12 +172,18 @@ 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 (!allowedSelectors[bytes4(data[:4])]) revert ResolverFunctionNotSupported(); + 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]) revert TextRecordNotSupported(); + if (!allowedTextRecords[key]) return abi.encode(""); } bytes memory callData = abi.encodeCall(IResolverService.resolve, (name, data)); diff --git a/test/Deploy/DeployL1.t.sol b/test/Deploy/DeployL1.t.sol index dac9ad48..d5f609a5 100644 --- a/test/Deploy/DeployL1.t.sol +++ b/test/Deploy/DeployL1.t.sol @@ -19,6 +19,8 @@ contract DeployL1Test is DeployL1, FnameResolverTestSuite { (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, @@ -32,6 +34,8 @@ 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); diff --git a/test/FnameResolver/FnameResolver.t.sol b/test/FnameResolver/FnameResolver.t.sol index bdeb555e..39561da9 100644 --- a/test/FnameResolver/FnameResolver.t.sol +++ b/test/FnameResolver/FnameResolver.t.sol @@ -29,6 +29,14 @@ contract FnameResolverTest is FnameResolverTestSuite { assertEq(resolver.VERSION(), "2023.08.23"); } + function testName() public { + assertEq(resolver.dnsEncodedName(), PARENT_DNS_ENCODED_NAME); + } + + function testPassthroughResolver() public { + assertEq(address(resolver.passthroughResolver()), address(PASSTHROUGH_RESOLVER)); + } + /*////////////////////////////////////////////////////////////// RESOLVE //////////////////////////////////////////////////////////////*/ @@ -87,7 +95,7 @@ contract FnameResolverTest is FnameResolverTestSuite { resolver.resolve(name, data); } - function testFuzzResolveTextRecordNotSupported( + function testFuzzResolveEmptyForUnsupportedTextRecord( string memory key ) public { // Calldata for the text(node, key) function (signature 0x59d1d43c) @@ -96,8 +104,19 @@ contract FnameResolverTest is FnameResolverTestSuite { string[] memory urls = new string[](1); urls[0] = FNAME_SERVER_URL; - vm.expectRevert(FnameResolver.TextRecordNotSupported.selector); - resolver.resolve(DNS_ENCODED_NAME, textCallData); + bytes memory result = resolver.resolve(DNS_ENCODED_NAME, textCallData); + assertEq(result, abi.encode("")); + } + + function testFuzzResolvePassthrough( + string memory key + ) public { + // namehash("farcaster.eth") + bytes32 node = 0x69d89a3b352fc56b7b2f65be229e08de44303dab8b7fd10e9f104766f17bdf29; + bytes memory textCallData = abi.encodeWithSelector(0x59d1d43c, node, key); + + bytes memory result = resolver.passthroughResolver().resolve(PARENT_DNS_ENCODED_NAME, textCallData); + assertEq(result, abi.encode("farcaster")); } /*////////////////////////////////////////////////////////////// diff --git a/test/FnameResolver/FnameResolverTestSuite.sol b/test/FnameResolver/FnameResolverTestSuite.sol index dddc9625..d82b17df 100644 --- a/test/FnameResolver/FnameResolverTestSuite.sol +++ b/test/FnameResolver/FnameResolverTestSuite.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.21; import {EIP712} from "openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {TestSuiteSetup} from "../TestSuiteSetup.sol"; -import {FnameResolver} from "../../src/FnameResolver.sol"; +import {FnameResolver, IExtendedResolver} from "../../src/FnameResolver.sol"; /* solhint-disable state-visibility */ @@ -14,14 +14,18 @@ abstract contract FnameResolverTestSuite is TestSuiteSetup { string internal constant FNAME_SERVER_URL = "https://fnames.fcast.id/ccip/{sender}/{data}.json"; /** - * @dev DNS-encoding of "alice.fcast.id". The DNS-encoded name consists of: - * - 1 byte for the length of the first label (5) - * - 5 bytes for the label ("alice") - * - 1 byte for the length of the second label (5) - * - 5 bytes for the label ("fcast") - * - 1 byte for the length of the third label (2) - * - 2 bytes for the label ("id") - * - A null byte terminating the encoded name. + * @dev DNS-encoding of "farcaster.eth" + */ + bytes internal constant PARENT_DNS_ENCODED_NAME = hex"096661726361737465720365746800"; + + /** + * @dev Address of the passthrough resolver, which must also support ENSIP-10. + * This will likely be the ENS Public Resolver. + */ + IExtendedResolver internal PASSTHROUGH_RESOLVER = new MockResolver(); + + /** + * @dev DNS-encoding of "alice.farcaster.eth" */ bytes internal constant DNS_ENCODED_NAME = hex"05616c696365096661726361737465720365746800"; @@ -45,7 +49,7 @@ abstract contract FnameResolverTestSuite is TestSuiteSetup { function setUp() public virtual override { (signer, signerPk) = makeAddrAndKey("signer"); (mallory, malloryPk) = makeAddrAndKey("mallory"); - resolver = new FnameResolver(FNAME_SERVER_URL, signer, owner); + resolver = new FnameResolver(PARENT_DNS_ENCODED_NAME, PASSTHROUGH_RESOLVER, FNAME_SERVER_URL, signer, owner); } /*////////////////////////////////////////////////////////////// @@ -74,3 +78,25 @@ abstract contract FnameResolverTestSuite is TestSuiteSetup { assertEq(signature.length, 65); } } + +contract MockResolver is IExtendedResolver { + function resolve(bytes calldata, bytes calldata data) external view returns (bytes memory) { + // text(node, key) + if (bytes4(data[:4]) == 0x59d1d43c) { + return abi.encode("farcaster"); + } + + // addr(node) + if (bytes4(data[:4]) == 0x3b3b57de) { + return abi.encode(address(this)); + } + + // addr(node, cointype) + if (bytes4(data[:4]) == 0xf1cb7e06) { + return abi.encode(abi.encodePacked(address(this))); + } + + // covers empty result for all standard resolver methods + return new bytes(64); + } +}