From b964860d18f2bc59b0f270ccabc0807ac6b89fe2 Mon Sep 17 00:00:00 2001 From: tate Date: Wed, 9 Oct 2024 16:44:48 +1100 Subject: [PATCH] test checkpoint --- contracts/test/TestBytesArrayValidator.sol | 79 + contracts/test/TestUserCallbackFunctions.sol | 32 + .../test/mocks/DummyAddrOffchainResolver.sol | 50 + .../test/mocks/DummyOffchainResolver.sol | 36 +- .../universalResolver/BytesArrayValidator.sol | 50 +- .../ENSIP10ResolverFinder.sol | 1 + contracts/universalResolver/ERC3668Caller.sol | 138 +- .../ERC3668Multicallable.sol | 11 +- .../universalResolver/UniversalResolver3.sol | 115 +- contracts/utils/PrefixlessHexStrings.sol | 53 + test/dnssec-oracle/TestSolidityTests.ts | 2 + test/utils/TestUniversalResolver3.ts | 2767 +++++++++++++++++ 12 files changed, 3191 insertions(+), 143 deletions(-) create mode 100644 contracts/test/TestBytesArrayValidator.sol create mode 100644 contracts/test/TestUserCallbackFunctions.sol create mode 100644 contracts/test/mocks/DummyAddrOffchainResolver.sol create mode 100644 contracts/utils/PrefixlessHexStrings.sol create mode 100644 test/utils/TestUniversalResolver3.ts diff --git a/contracts/test/TestBytesArrayValidator.sol b/contracts/test/TestBytesArrayValidator.sol new file mode 100644 index 00000000..60c5b16d --- /dev/null +++ b/contracts/test/TestBytesArrayValidator.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "../universalResolver/BytesArrayValidator.sol"; + +contract TestBytesArrayValidator { + function testValidBytesArray() public pure { + // Create a valid bytes array + bytes[] memory validArray = new bytes[](5); + validArray[0] = "Hello"; + validArray[1] = "World"; + validArray[2] = "Two"; + validArray[3] = "Three"; + validArray[4] = "Four"; + bytes memory encodedValidArray = abi.encode(validArray); + + bool isValid = BytesArrayValidator.isValidBytesArray(encodedValidArray); + require(isValid, "Should be a valid bytes array"); + } + + function testInvalidBytesArray() public pure { + // Create an invalid bytes array (too short) + bytes memory invalidArray = new bytes(16); + + bool isValid = BytesArrayValidator.isValidBytesArray(invalidArray); + require(!isValid, "Should be an invalid bytes array"); + } + + function testEmptyBytesArray() public pure { + // Create an empty bytes array + bytes[] memory emptyArray = new bytes[](0); + bytes memory encodedEmptyArray = abi.encode(emptyArray); + + bool isValid = BytesArrayValidator.isValidBytesArray(encodedEmptyArray); + require(isValid, "Empty array should be valid"); + } + + function testEmptyItemInBytesArray() public pure { + // Create an empty bytes array + bytes[] memory emptyArray = new bytes[](1); + emptyArray[0] = ""; + bytes memory encodedEmptyArray = abi.encode(emptyArray); + + bool isValid = BytesArrayValidator.isValidBytesArray(encodedEmptyArray); + require(isValid, "Empty array should be valid"); + } + + function largeEmptyBytesArray() public pure { + bytes[] memory largeArray = new bytes[](1000); + bytes memory encodedLargeArray = abi.encode(largeArray); + + bool isValid = BytesArrayValidator.isValidBytesArray(encodedLargeArray); + require(isValid, "Large array should be valid"); + } + + function testLargeBytesArray() public pure { + // Create a large bytes array + bytes[] memory largeArray = new bytes[](1000); + for (uint i = 0; i < 1000; i++) { + largeArray[i] = new bytes(100); + } + bytes memory encodedLargeArray = abi.encode(largeArray); + + bool isValid = BytesArrayValidator.isValidBytesArray(encodedLargeArray); + require(isValid, "Large array should be valid"); + } + + function testInvalidOffsets() public pure { + // Create an invalid bytes array with incorrect offsets + bytes memory invalidOffsets = new bytes(128); + // Set an invalid offset + assembly { + mstore(add(invalidOffsets, 64), 0x20) + } + + bool isValid = BytesArrayValidator.isValidBytesArray(invalidOffsets); + require(!isValid, "Array with invalid offsets should be invalid"); + } +} diff --git a/contracts/test/TestUserCallbackFunctions.sol b/contracts/test/TestUserCallbackFunctions.sol new file mode 100644 index 00000000..da65fa44 --- /dev/null +++ b/contracts/test/TestUserCallbackFunctions.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "../universalResolver/ERC3668Caller.sol"; +import "hardhat/console.sol"; + +contract TestUserCallbackFunctions is ERC3668Caller { + function testCreateUserCallbackFunctions() public view { + bytes4 internalCallbackFunction = bytes4(0x11111111); + bytes4 calldataRewriteFunction = bytes4(0x22222222); + bytes4 failureCallbackFunction = bytes4(0x33333333); + bytes4 validateResponseFunction = bytes4(0x44444444); + + uint256 gasBefore = gasleft(); + uint256 callbackFunctions = createUserCallbackFunctions( + internalCallbackFunction, + calldataRewriteFunction, + failureCallbackFunction, + validateResponseFunction + ); + uint256 gasAfter = gasleft(); + console.log("gas", gasBefore - gasAfter); + + console.logBytes32(bytes32(callbackFunctions)); + + require( + callbackFunctions == + 0x0000000000000000000000000000000044444444333333332222222211111111, + "Callback functions should be correct" + ); + } +} diff --git a/contracts/test/mocks/DummyAddrOffchainResolver.sol b/contracts/test/mocks/DummyAddrOffchainResolver.sol new file mode 100644 index 00000000..007b450f --- /dev/null +++ b/contracts/test/mocks/DummyAddrOffchainResolver.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "../../../contracts/resolvers/profiles/IAddrResolver.sol"; +import "../../../contracts/resolvers/profiles/IExtendedResolver.sol"; + +error OffchainLookup( + address sender, + string[] urls, + bytes callData, + bytes4 callbackFunction, + bytes extraData +); + +contract DummyAddrOffchainResolver is IAddrResolver, ERC165 { + function supportsInterface( + bytes4 interfaceId + ) public view virtual override returns (bool) { + return + interfaceId == type(IAddrResolver).interfaceId || + super.supportsInterface(interfaceId); + } + + function addr(bytes32) external view returns (address payable) { + string[] memory urls = new string[](1); + urls[0] = "https://example.com/"; + + bytes memory data = abi.encode(address(this)); + + revert OffchainLookup( + address(this), + urls, + data, + this.addrCallback.selector, + data + ); + } + + function addrOnchain(bytes32) external view returns (address) { + return address(this); + } + + function addrCallback( + bytes calldata response, + bytes calldata extraData + ) external view returns (address) { + return abi.decode(response, (address)); + } +} diff --git a/contracts/test/mocks/DummyOffchainResolver.sol b/contracts/test/mocks/DummyOffchainResolver.sol index 83780967..6224db75 100644 --- a/contracts/test/mocks/DummyOffchainResolver.sol +++ b/contracts/test/mocks/DummyOffchainResolver.sol @@ -48,14 +48,34 @@ contract DummyOffchainResolver is IExtendedResolver, ERC165 { function resolveCallback( bytes calldata response, bytes calldata extraData - ) external view returns (bytes memory) { - require( - keccak256(response) == keccak256(extraData), - "Response data error" - ); - if (bytes4(extraData) == bytes4(keccak256("name(bytes32)"))) { - return abi.encode("offchain.test.eth"); + ) external pure returns (bytes memory) { + if (bytes4(extraData) == bytes4(keccak256("pubkey(bytes32)"))) { + revert(); + } + if (bytes4(extraData) == bytes4(keccak256("contenthash(bytes32)"))) { + revert("Unsupported call"); + } + if (bytes4(extraData) != bytes4(keccak256("multicall(bytes[])"))) { + return response; + } + + bytes[] memory results = abi.decode(response, (bytes[])); + bytes[] memory calls = abi.decode(extraData[4:], (bytes[])); + for (uint256 i = 0; i < calls.length; i++) { + if (bytes4(calls[i]) == bytes4(keccak256("text(bytes32,string)"))) { + calls[i] = results[i]; + } else if (bytes4(calls[i]) == bytes4(keccak256("addr(bytes32)"))) { + calls[i] = results[i]; + } else if (bytes4(calls[i]) == bytes4(keccak256("name(bytes32)"))) { + return ""; + } else if ( + bytes4(calls[i]) == bytes4(keccak256("pubkey(bytes32)")) + ) { + calls[i] = ""; + } else { + revert("Unsupported call"); + } } - return abi.encode(address(this)); + return abi.encode(calls); } } diff --git a/contracts/universalResolver/BytesArrayValidator.sol b/contracts/universalResolver/BytesArrayValidator.sol index 839d2877..91900f4b 100644 --- a/contracts/universalResolver/BytesArrayValidator.sol +++ b/contracts/universalResolver/BytesArrayValidator.sol @@ -4,59 +4,49 @@ pragma solidity ^0.8.4; library BytesArrayValidator { function isValidBytesArray(bytes memory data) internal pure returns (bool) { // The data must be at least 32 bytes long to contain the array length - if (data.length < 32) { - return false; - } + if (data.length < 32) return false; - uint256 N; // Number of elements in the bytes[] array + uint256 arrayLength; assembly { - N := mload(add(data, 32)) // Read the array length from data[32..63] + arrayLength := mload(add(data, 64)) } - // Limit N to a reasonable size to prevent excessive computation - if (N > 1e6) { - return false; - } + // Limit length to a reasonable size to prevent excessive computation + if (arrayLength > 1e6) return false; - uint256 offsetsStart = 32; // The offsets start after the array length - uint256 offsetsEnd = offsetsStart + N * 32; + uint256 offsetsStart = 64; + uint256 offsetsEnd = offsetsStart + arrayLength * 32; - // The data must be long enough to include all offsets - if (data.length < offsetsEnd) { - return false; + if (arrayLength > 0) { + if (data.length < offsetsEnd) return false; + } else { + if (data.length != 64) return false; } // Loop through each offset and validate the corresponding data - for (uint256 i = 0; i < N; i++) { + for (uint256 i = 0; i < arrayLength; i++) { uint256 offset_i; assembly { - // Read the i-th offset from data[32 + i*32 .. 32 + (i+1)*32 - 1] - offset_i := mload(add(add(data, offsetsStart), mul(32, i))) - } - - // Offsets should point beyond the header (array length and offsets) - if (offset_i < offsetsEnd) { - return false; + offset_i := mload( + add(add(data, 32), add(offsetsStart, mul(i, 32))) + ) } // The offset plus 32 bytes must be within the data length to read the length of the element - if (offset_i + 32 > data.length) { - return false; - } + if (offsetsStart + offset_i + 32 > data.length) return false; uint256 Li; // Length of the i-th byte array assembly { // Read the length of the element from data[32 + offset_i .. 32 + offset_i + 31] - Li := mload(add(add(data, 32), offset_i)) + Li := mload(add(add(add(data, 32), offset_i), offsetsStart)) } // Calculate the end position of the element's data - uint256 elementDataEnd = offset_i + 32 + Li; + uint256 elementDataEnd = offset_i + + (Li % 32 == 0 ? Li : Li + (32 - (Li % 32))); // The element data must be within the bounds of the data array - if (elementDataEnd > data.length) { - return false; - } + if (elementDataEnd > data.length) return false; } // All checks passed; the data is a valid ABI-encoded bytes[] diff --git a/contracts/universalResolver/ENSIP10ResolverFinder.sol b/contracts/universalResolver/ENSIP10ResolverFinder.sol index d99ba92f..5a89afdf 100644 --- a/contracts/universalResolver/ENSIP10ResolverFinder.sol +++ b/contracts/universalResolver/ENSIP10ResolverFinder.sol @@ -40,6 +40,7 @@ abstract contract ENSIP10ResolverFinder { if (labelLength == 0) { return (address(0), bytes32(0), offset); } + // TODO: ensure name has enough length to avoid out-of-bounds uint256 nextLabel = offset + labelLength + 1; bytes32 labelHash; if ( diff --git a/contracts/universalResolver/ERC3668Caller.sol b/contracts/universalResolver/ERC3668Caller.sol index bedda865..823663dd 100644 --- a/contracts/universalResolver/ERC3668Caller.sol +++ b/contracts/universalResolver/ERC3668Caller.sol @@ -6,6 +6,22 @@ import {ERC3668Utils, OffchainLookup, OffchainLookupData} from "../utils/ERC3668 error LookupSenderMismatched(); +error HttpError(uint16 status, string message); + +// User callback functions are encoded as a single bytes32 value +// The first 4 bytes are the internal callback function +// The next 4 bytes are the calldata rewrite function +// The next 4 bytes are the failure callback function +// The next 4 bytes are the validate response function +// The next 4 bytes are the external callback function +// The remaining 12 bytes are unused +uint16 constant INTERNAL_CALLBACK_FUNCTION_OFFSET = 0; +uint16 constant CALLDATA_REWRITE_FUNCTION_OFFSET = 32; +uint16 constant FAILURE_CALLBACK_FUNCTION_OFFSET = 64; +uint16 constant VALIDATE_RESPONSE_FUNCTION_OFFSET = 96; +uint16 constant EXTERNAL_CALLBACK_FUNCTION_OFFSET = 128; +uint32 constant FUNCTIONS_MASK = 0xffffffff; + abstract contract ERC3668Caller { function callback( bytes calldata response, @@ -13,13 +29,26 @@ abstract contract ERC3668Caller { ) external view returns (bytes memory) { ( address target, - bytes4 internalCallbackFunction, - bytes4 externalCallbackFunction, - bytes4 calldataRewriteFunction, - bytes4 failureCallbackFunction, + uint256 callbackFunctions, bytes memory internalExtraData, bytes memory externalExtraData ) = _getExtraData(extraData); + + bytes4 validateResponseFunction = bytes4( + uint32( + (callbackFunctions >> VALIDATE_RESPONSE_FUNCTION_OFFSET) & + FUNCTIONS_MASK + ) + ); + if (validateResponseFunction != bytes4(0)) + _validateResponse(validateResponseFunction, response); + + bytes4 externalCallbackFunction = bytes4( + uint32( + (callbackFunctions >> EXTERNAL_CALLBACK_FUNCTION_OFFSET) & + FUNCTIONS_MASK + ) + ); return call( target, @@ -29,9 +58,7 @@ abstract contract ERC3668Caller { abi.encode(response, externalExtraData) ), internalExtraData, - internalCallbackFunction, - calldataRewriteFunction, - failureCallbackFunction + uint128(callbackFunctions) ); } @@ -40,33 +67,18 @@ abstract contract ERC3668Caller { uint256 gas, bytes memory data, bytes memory internalExtraData, - bytes4 internalCallbackFunction - ) internal view returns (bytes memory) { - return - call( - target, - gas, - data, - internalExtraData, - internalCallbackFunction, - bytes4(0), - bytes4(0) - ); - } - - function call( - address target, - uint256 gas, - bytes memory data, - bytes memory internalExtraData, - bytes4 internalCallbackFunction, - bytes4 calldataRewriteFunction, - bytes4 failureCallbackFunction + uint128 userCallbackFunctions ) internal view returns (bytes memory) { (bool success, bytes4 errorId, bytes memory result) = ERC3668Utils .callWithNormalisedResult(target, data, gas); if (success) { + bytes4 internalCallbackFunction = bytes4( + uint32( + (userCallbackFunctions >> + INTERNAL_CALLBACK_FUNCTION_OFFSET) & FUNCTIONS_MASK + ) + ); _internalCallback( internalCallbackFunction, result, @@ -82,14 +94,18 @@ abstract contract ERC3668Caller { bytes memory extraData = _createExtraData( target, - internalCallbackFunction, + userCallbackFunctions, lookupData.callbackFunction, - calldataRewriteFunction, - failureCallbackFunction, internalExtraData, lookupData.extraData ); + bytes4 calldataRewriteFunction = bytes4( + uint32( + (userCallbackFunctions >> + CALLDATA_REWRITE_FUNCTION_OFFSET) & FUNCTIONS_MASK + ) + ); if (calldataRewriteFunction != bytes4(0)) { lookupData.callData = abi.decode( _formatCalldata(calldataRewriteFunction, lookupData), @@ -111,6 +127,12 @@ abstract contract ERC3668Caller { ? bytes("") : bytes.concat(errorId, result); + bytes4 failureCallbackFunction = bytes4( + uint32( + (userCallbackFunctions >> FAILURE_CALLBACK_FUNCTION_OFFSET) & + FUNCTIONS_MASK + ) + ); if (failureCallbackFunction != bytes4(0)) { _internalCallback( failureCallbackFunction, @@ -122,6 +144,22 @@ abstract contract ERC3668Caller { LowLevelCallUtils.propagateRevert(errorData); } + function createUserCallbackFunctions( + bytes4 internalCallbackFunction, + bytes4 calldataRewriteFunction, + bytes4 failureCallbackFunction, + bytes4 validateResponseFunction + ) internal pure returns (uint128) { + return + (uint128(uint32(validateResponseFunction)) << + VALIDATE_RESPONSE_FUNCTION_OFFSET) | + (uint128(uint32(failureCallbackFunction)) << + FAILURE_CALLBACK_FUNCTION_OFFSET) | + (uint128(uint32(calldataRewriteFunction)) << + CALLDATA_REWRITE_FUNCTION_OFFSET) | + uint128(uint32(internalCallbackFunction)); + } + function _formatCalldata( bytes4 calldataRewriteFunction, OffchainLookupData memory data @@ -139,6 +177,18 @@ abstract contract ERC3668Caller { ); } + function _validateResponse( + bytes4 validateResponseFunction, + bytes memory response + ) private view { + bool success = LowLevelCallUtils.functionStaticCall( + address(this), + abi.encodeWithSelector(validateResponseFunction, response) + ); + if (!success) LowLevelCallUtils.propagateRevert(); + return; + } + function _internalCallback( bytes4 callbackFunction, bytes memory response, @@ -154,20 +204,17 @@ abstract contract ERC3668Caller { function _createExtraData( address target, - bytes4 internalCallbackFunction, + uint128 userCallbackFunctions, bytes4 externalCallbackFunction, - bytes4 calldataRewriteFunction, - bytes4 failureCallbackFunction, bytes memory internalExtraData, bytes memory externalExtraData - ) private pure returns (bytes memory) { + ) private view returns (bytes memory) { + uint256 callbackFunctions = uint256(userCallbackFunctions) | + (uint256(uint32(externalCallbackFunction)) << 128); return abi.encode( target, - internalCallbackFunction, - externalCallbackFunction, - calldataRewriteFunction, - failureCallbackFunction, + callbackFunctions, internalExtraData, externalExtraData ); @@ -180,18 +227,11 @@ abstract contract ERC3668Caller { pure returns ( address target, - bytes4 internalCallbackFunction, - bytes4 externalCallbackFunction, - bytes4 calldataRewriteFunction, - bytes4 failureCallbackFunction, + uint256 callbackFunctions, bytes memory internalExtraData, bytes memory externalExtraData ) { - return - abi.decode( - extraData, - (address, bytes4, bytes4, bytes4, bytes4, bytes, bytes) - ); + return abi.decode(extraData, (address, uint256, bytes, bytes)); } } diff --git a/contracts/universalResolver/ERC3668Multicallable.sol b/contracts/universalResolver/ERC3668Multicallable.sol index d0726dfb..8b8798ed 100644 --- a/contracts/universalResolver/ERC3668Multicallable.sol +++ b/contracts/universalResolver/ERC3668Multicallable.sol @@ -23,25 +23,26 @@ abstract contract ERC3668Multicallable { bytes[] memory results = abi.decode(response, (bytes[])); (InternalResult[] memory internalResults, string[] memory urls) = abi .decode(extraData, (InternalResult[], string[])); - uint256 offchainCount = 0; - + uint256 existingOffchainCount = 0; + uint256 newOffchainCount = 0; for (uint256 i = 0; i < internalResults.length; i++) { if (!internalResults[i].offchain) continue; (bytes4 callbackFunction, bytes memory wrappedExtraData) = abi .decode(internalResults[i].data, (bytes4, bytes)); bytes memory callData = abi.encodeWithSelector( callbackFunction, - results[offchainCount], + results[existingOffchainCount], wrappedExtraData ); internalResults[i] = callWithInternalResult( address(this), callData ); - if (internalResults[i].offchain) offchainCount += 1; + existingOffchainCount += 1; + if (internalResults[i].offchain) newOffchainCount += 1; } - return _getResults(internalResults, offchainCount, urls); + return _getResults(internalResults, newOffchainCount, urls); } function multicall( diff --git a/contracts/universalResolver/UniversalResolver3.sol b/contracts/universalResolver/UniversalResolver3.sol index cd696e0a..b425a951 100644 --- a/contracts/universalResolver/UniversalResolver3.sol +++ b/contracts/universalResolver/UniversalResolver3.sol @@ -2,7 +2,7 @@ pragma solidity >=0.8.17 <0.9.0; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {PrefixlessHexStrings} from "../utils/PrefixlessHexStrings.sol"; import {ENS} from "../registry/ENS.sol"; import {ERC3668Multicallable, MulticallableGateway} from "./ERC3668Multicallable.sol"; import {ERC3668Caller} from "./ERC3668Caller.sol"; @@ -51,7 +51,8 @@ contract UniversalResolver3 is ERC3668Caller, ENSIP10ResolverFinder { - using Strings for uint256; + using PrefixlessHexStrings for uint256; + using PrefixlessHexStrings for bytes; using Address for address; using NameEncoder for string; using HexUtils for bytes; @@ -160,14 +161,17 @@ contract UniversalResolver3 is gateways, isSingleInternallyEncodedCall ), - this.resolveMulticallResolveCallback.selector, - bytes4(0), - this.resolveMulticallResolveCallback.selector + createUserCallbackFunctions( + this.resolveMulticallResolveCallback.selector, + bytes4(0), + this.resolveMulticallResolveCallback.selector, + bytes4(0) + ) ); } function resolveMulticallResolveCallback( - bytes calldata response, + bytes memory response, bytes calldata extraData ) external view returns (bytes memory, address) { ( @@ -178,7 +182,13 @@ contract UniversalResolver3 is bool isSingleInternallyEncodedCall ) = abi.decode(extraData, (bytes, bytes[], address, string[], bool)); - if (!BytesArrayValidator.isValidBytesArray(response)) { + if ( + _isErrorResult(response) || + _isEmptyResult(response) || + !BytesArrayValidator.isValidBytesArray( + response = abi.decode(response, (bytes)) + ) + ) return _internalMulticall( name, @@ -188,13 +198,13 @@ contract UniversalResolver3 is isSingleInternallyEncodedCall, true ); - } if (isSingleInternallyEncodedCall) return ( - _resultFromSingleInternallyEncodedCall(response, true), + _resultFromSingleInternallyEncodedCall(response, false), resolver ); + return (response, resolver); } @@ -251,14 +261,14 @@ contract UniversalResolver3 is isSingleInternallyEncodedCall, isExtendedResolver ), - this.internalMulticallResolveCallback.selector + uint32(this.internalMulticallResolveCallback.selector) ); } function internalMulticallResolveCallback( bytes calldata response, bytes calldata extraData - ) external pure returns (bytes memory, address) { + ) external view returns (bytes memory, address) { ( address resolver, bool isSingleInternallyEncodedCall, @@ -291,7 +301,7 @@ contract UniversalResolver3 is function _internalCallCallback( bytes memory response, bytes calldata /* extraData */ - ) external pure returns (bytes memory) { + ) external view returns (bytes memory) { assembly { return(add(response, 32), mload(response)) } @@ -299,7 +309,7 @@ contract UniversalResolver3 is function _internalCallCalldataRewrite( OffchainLookupData memory data - ) external pure returns (bytes memory) { + ) external view returns (bytes memory) { return abi.encodeWithSelector( BatchGateway2.query.selector, @@ -309,6 +319,18 @@ contract UniversalResolver3 is ); } + function _internalCallValidateResponse( + bytes calldata response + ) external view { + if (bytes4(response) == HttpError.selector) { + (uint16 status, string memory message) = abi.decode( + response[4:], + (uint16, string) + ); + revert HttpError(status, message); + } + } + function _internalCall( address target, bytes calldata data, @@ -320,9 +342,12 @@ contract UniversalResolver3 is gas, data, "", - this._internalCallCallback.selector, - this._internalCallCalldataRewrite.selector, - bytes4(0) + createUserCallbackFunctions( + this._internalCallCallback.selector, + this._internalCallCalldataRewrite.selector, + bytes4(0), + this._internalCallValidateResponse.selector + ) ); } @@ -366,7 +391,7 @@ contract UniversalResolver3 is 0, encodedCall, abi.encode(lookupAddress, coinType, gateways), - this.forwardLookupReverseCallback.selector + uint32(this.forwardLookupReverseCallback.selector) ); } @@ -397,6 +422,14 @@ contract UniversalResolver3 is addrCall, gateways ); + uint128 userCallbackFunctions = createUserCallbackFunctions( + this.processLookupReverseCallback.selector, + bytes4(0), + coinType == 60 + ? this.attemptAddrResolverReverseCallback.selector + : bytes4(0), + bytes4(0) + ); call( address(this), 0, @@ -409,11 +442,7 @@ contract UniversalResolver3 is reverseResolver, false ), - this.processLookupReverseCallback.selector, - bytes4(0), - coinType == 60 - ? this.attemptAddrResolverReverseCallback.selector - : bytes4(0) + userCallbackFunctions ); } @@ -452,7 +481,7 @@ contract UniversalResolver3 is reverseResolver, true ), - this.processLookupReverseCallback.selector + uint32(this.processLookupReverseCallback.selector) ); } @@ -475,11 +504,11 @@ contract UniversalResolver3 is response, (bytes, address) ); + bytes memory unwrappedResult = isAddrCall + ? abi.encodePacked(abi.decode(result, (address))) + : abi.decode(result, (bytes)); if (_isEvmChain(coinType)) { - bytes memory unwrappedResultMaybe = isAddrCall - ? abi.encodePacked(abi.decode(result, (address))) - : abi.decode(result, (bytes)); - address resolvedAddress = _bytesToAddress(unwrappedResultMaybe); + address resolvedAddress = _bytesToAddress(unwrappedResult); address decodedLookupAddress = _bytesToAddress(lookupAddress); if (resolvedAddress != decodedLookupAddress) { revert ReverseAddressMismatch( @@ -487,8 +516,8 @@ contract UniversalResolver3 is ); } } else { - if (keccak256(result) != keccak256(lookupAddress)) { - revert ReverseAddressMismatch(result); + if (keccak256(unwrappedResult) != keccak256(lookupAddress)) { + revert ReverseAddressMismatch(unwrappedResult); } } return (resolvedName, reverseResolver, resolver); @@ -518,8 +547,8 @@ contract UniversalResolver3 is } function _resultFromSingleInternallyEncodedCall( - bytes calldata result, - bool isExtendedResolver + bytes memory result, + bool shouldDecodeResult ) internal pure returns (bytes memory) { bytes[] memory results = abi.decode(result, (bytes[])); bytes memory item = results[0]; @@ -531,7 +560,7 @@ contract UniversalResolver3 is revert ResolverError(item); } - if (isExtendedResolver) return abi.decode(item, (bytes)); + if (shouldDecodeResult) return abi.decode(item, (bytes)); return item; } @@ -552,15 +581,13 @@ contract UniversalResolver3 is function _createReverseNode( bytes memory lookupAddress, uint256 coinType - ) internal pure returns (string memory) { + ) internal view returns (string memory) { return string( bytes.concat( - _bytesToHexStringBytes(lookupAddress), + lookupAddress.toHexString(), ".", - coinType == 60 - ? bytes("addr") - : bytes(coinType.toHexString()), + coinType == 60 ? bytes("addr") : coinType.toHexString(), ".reverse" ) ); @@ -577,18 +604,4 @@ contract UniversalResolver3 is a := div(mload(add(b, 32)), exp(256, 12)) } } - - function _bytesToHexStringBytes( - bytes memory data - ) internal pure returns (bytes memory) { - bytes memory result = new bytes(data.length * 2); - bytes16 _base = "0123456789abcdef"; - - for (uint256 i = 0; i < data.length; i++) { - result[i * 2] = _base[uint8(data[i]) / 16]; - result[i * 2 + 1] = _base[uint8(data[i]) % 16]; - } - - return result; - } } diff --git a/contracts/utils/PrefixlessHexStrings.sol b/contracts/utils/PrefixlessHexStrings.sol new file mode 100644 index 00000000..29f96b23 --- /dev/null +++ b/contracts/utils/PrefixlessHexStrings.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Strings.sol) + +pragma solidity ^0.8.0; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +/** + * @dev String operations. + */ +library PrefixlessHexStrings { + bytes16 private constant _SYMBOLS = "0123456789abcdef"; + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. + */ + function toHexString(uint256 value) internal pure returns (bytes memory) { + unchecked { + return toHexString(value, Math.log256(value) + 1); + } + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + */ + function toHexString( + uint256 value, + uint256 length + ) internal pure returns (bytes memory) { + bytes memory buffer = new bytes(2 * length); + for (uint256 i = 2 * length; i > 0; --i) { + buffer[i - 1] = _SYMBOLS[value & 0xf]; + value >>= 4; + } + require(value == 0, "Strings: hex length insufficient"); + return buffer; + } + + /** + * @dev Converts arbitrary bytes to its ASCII `string` hexadecimal representation. + */ + function toHexString( + bytes memory input + ) internal view returns (bytes memory) { + bytes memory buffer = new bytes(input.length * 2); + for (uint256 i = input.length; i > 0; --i) { + uint8 value = uint8(input[i - 1]); + buffer[i * 2 - 2] = _SYMBOLS[value >> 4]; + buffer[i * 2 - 1] = _SYMBOLS[value & 0xf]; + } + return buffer; + } +} diff --git a/test/dnssec-oracle/TestSolidityTests.ts b/test/dnssec-oracle/TestSolidityTests.ts index b2915c26..5d04dc1f 100644 --- a/test/dnssec-oracle/TestSolidityTests.ts +++ b/test/dnssec-oracle/TestSolidityTests.ts @@ -3,6 +3,8 @@ import hre from 'hardhat' const contracts = [ 'contracts/test/TestBytesUtils.sol:TestBytesUtils', 'contracts/test/TestRRUtils.sol:TestRRUtils', + 'contracts/test/TestBytesArrayValidator.sol:TestBytesArrayValidator', + 'contracts/test/TestUserCallbackFunctions.sol:TestUserCallbackFunctions', ] as const contracts.forEach((contract) => { diff --git a/test/utils/TestUniversalResolver3.ts b/test/utils/TestUniversalResolver3.ts new file mode 100644 index 00000000..1a4f35e5 --- /dev/null +++ b/test/utils/TestUniversalResolver3.ts @@ -0,0 +1,2767 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import hre from 'hardhat' +import { + decodeFunctionResult, + encodeAbiParameters, + encodeErrorResult, + encodeFunctionData, + encodeFunctionResult, + encodePacked, + getAddress, + hexToBigInt, + labelhash, + namehash, + parseAbi, + parseAbiParameters, + toBytes, + toFunctionSelector, + toHex, + zeroAddress, + zeroHash, + type Address, + type Hex, + type ReadContractReturnType, +} from 'viem' +import { optimism } from 'viem/chains' +import { encodedRealAnchors } from '../fixtures/anchors.js' +import { dnsEncodeName } from '../fixtures/dnsEncodeName.js' +import { getReverseNodeHash } from '../fixtures/getReverseNode.js' + +const emptyBytes4 = '0x00000000' + +const encodeResolveMulticallExtraData = ({ + name, + calls, + resolverAddress, + gateways, + isSingleInternallyEncodedCall, +}: { + name: Hex + calls: Hex[] + resolverAddress: Address + gateways: string[] + isSingleInternallyEncodedCall: boolean +}) => + encodeAbiParameters( + parseAbiParameters('bytes, bytes[], address, string[], bool'), + [name, calls, resolverAddress, gateways, isSingleInternallyEncodedCall], + ) + +const encodeInternalMulticallExtraData = ({ + resolverAddress, + isSingleInternallyEncodedCall, + isExtendedResolver, +}: { + resolverAddress: Address + isSingleInternallyEncodedCall: boolean + isExtendedResolver: boolean +}) => + encodeAbiParameters(parseAbiParameters('address, bool, bool'), [ + resolverAddress, + isSingleInternallyEncodedCall, + isExtendedResolver, + ]) + +type SingleCallExtraData = { + resolverAddress: Address + internalCallbackFunction: Hex + externalCallbackFunction: Hex + calldataRewriteFunction: Hex + failureCallbackFunction: Hex + validateResponseFunction: Hex + internalExtraData: Hex + externalExtraData: Hex +} + +const encodeExtraData = ({ + resolverAddress, + internalCallbackFunction, + externalCallbackFunction, + calldataRewriteFunction, + failureCallbackFunction, + validateResponseFunction, + internalExtraData, + externalExtraData, +}: SingleCallExtraData) => + encodeAbiParameters( + [ + { name: 'target', type: 'address' }, + { name: 'callbackFunctions', type: 'uint256' }, + { name: 'internalExtraData', type: 'bytes' }, + { name: 'externalExtraData', type: 'bytes' }, + ], + [ + resolverAddress, + hexToBigInt( + encodePacked( + ['bytes4', 'bytes4', 'bytes4', 'bytes4', 'bytes4'], + [ + externalCallbackFunction, + validateResponseFunction, + failureCallbackFunction, + calldataRewriteFunction, + internalCallbackFunction, + ], + ), + ), + internalExtraData, + externalExtraData, + ], + ) + +const encodeMulticallExtraData = ({ + calls, + urls, +}: { + calls: ( + | { offchain: true; callbackFunction: Hex; data: Hex } + | { offchain: false; data: Hex } + )[] + urls: string[] +}) => + encodeAbiParameters(parseAbiParameters('(bool,bytes)[], string[]'), [ + calls.map( + (params) => + [ + params.offchain, + params.offchain + ? encodeAbiParameters(parseAbiParameters('bytes4,bytes'), [ + params.callbackFunction, + params.data, + ]) + : params.data, + ] as const, + ), + urls, + ]) + +const baseResolveMulticallExtraData = { + internalCallbackFunction: toFunctionSelector( + 'function resolveMulticallResolveCallback(bytes,bytes)', + ), + externalCallbackFunction: toFunctionSelector( + 'function resolveCallback(bytes,bytes)', + ), + calldataRewriteFunction: emptyBytes4, + failureCallbackFunction: toFunctionSelector( + 'function resolveMulticallResolveCallback(bytes,bytes)', + ), + validateResponseFunction: emptyBytes4, +} as const + +const baseInternalMulticallExtraData = { + internalCallbackFunction: toFunctionSelector( + 'function internalMulticallResolveCallback(bytes,bytes)', + ), + externalCallbackFunction: toFunctionSelector( + 'function multicallCallback(bytes,bytes)', + ), + calldataRewriteFunction: emptyBytes4, + failureCallbackFunction: emptyBytes4, + validateResponseFunction: emptyBytes4, +} as const + +const baseInternalCallExtraData = { + internalCallbackFunction: toFunctionSelector( + 'function _internalCallCallback(bytes,bytes)', + ), + calldataRewriteFunction: toFunctionSelector( + 'function _internalCallCalldataRewrite((address,string[],bytes,bytes4,bytes))', + ), + failureCallbackFunction: emptyBytes4, + validateResponseFunction: toFunctionSelector( + 'function _internalCallValidateResponse(bytes)', + ), + internalExtraData: '0x', +} as const + +const opChainId = optimism.id +const opCoinType = (0x80000000 | opChainId) >>> 0 +const opHexCoinType = toHex(toBytes(opCoinType)).slice(2) + +const solCoinType = 501 +const solHexCoinType = toHex(toBytes(solCoinType)).slice(2) +const solAddressHex = + '0x18f9d8d877393bbbe8d697a8a2e52879cc7e84f467656d1cce6bab5a8d2637ec' + +async function fixture() { + const accounts = await hre.viem + .getWalletClients() + .then((clients) => clients.map((c) => c.account)) + const ensRegistry = await hre.viem.deployContract('ENSRegistry', []) + const root = await hre.viem.deployContract('Root', [ensRegistry.address]) + const nameWrapper = await hre.viem.deployContract('DummyNameWrapper', []) + const reverseRegistrar = await hre.viem.deployContract('ReverseRegistrar', [ + ensRegistry.address, + ]) + + await root.write.setController([accounts[0].address, true]) + await ensRegistry.write.setOwner([zeroHash, root.address]) + + await root.write.setSubnodeOwner([labelhash('reverse'), accounts[0].address]) + await root.write.setSubnodeOwner([labelhash('eth'), accounts[0].address]) + await ensRegistry.write.setSubnodeOwner([ + namehash('reverse'), + labelhash('addr'), + reverseRegistrar.address, + ]) + + const publicResolver = await hre.viem.deployContract('PublicResolver', [ + ensRegistry.address, + nameWrapper.address, + zeroAddress, + zeroAddress, + ]) + const universalResolver = await hre.viem.deployContract( + 'UniversalResolver3', + [ensRegistry.address, ['http://universal-offchain-resolver.local']], + ) + const offchainResolver = await hre.viem.deployContract( + 'DummyOffchainResolver', + [], + ) + const addrOffchainResolver = await hre.viem.deployContract( + 'DummyAddrOffchainResolver', + [], + ) + const oldResolver = await hre.viem.deployContract('DummyOldResolver', []) + const revertResolver = await hre.viem.deployContract( + 'DummyRevertResolver', + [], + ) + const legacyResolver = await hre.viem.deployContract('LegacyResolver', []) + const createTestEthSub = async ({ + label, + resolverAddress = zeroAddress, + }: { + label: string + resolverAddress?: Address + }) => { + await ensRegistry.write.setSubnodeRecord([ + namehash('test.eth'), + labelhash(label), + accounts[0].address, + resolverAddress, + 0n, + ]) + } + + await ensRegistry.write.setSubnodeRecord([ + namehash('eth'), + labelhash('test'), + accounts[0].address, + publicResolver.address, + 0n, + ]) + await ensRegistry.write.setSubnodeRecord([ + namehash('eth'), + labelhash('oldprimary'), + accounts[0].address, + oldResolver.address, + 0n, + ]) + await ensRegistry.write.setSubnodeRecord([ + namehash('eth'), + labelhash('legacy-resolver'), + accounts[0].address, + legacyResolver.address, + 0n, + ]) + await ensRegistry.write.setSubnodeRecord([ + namehash('test.eth'), + labelhash('sub'), + accounts[0].address, + accounts[1].address, + 0n, + ]) + await createTestEthSub({ + label: 'offchain', + resolverAddress: offchainResolver.address, + }) + await createTestEthSub({ + label: 'addr-offchain', + resolverAddress: addrOffchainResolver.address, + }) + await createTestEthSub({ + label: 'no-resolver', + }) + await createTestEthSub({ + label: 'revert-resolver', + resolverAddress: revertResolver.address, + }) + await createTestEthSub({ + label: 'non-contract-resolver', + resolverAddress: accounts[0].address, + }) + + let name = 'test.eth' + for (let i = 0; i < 5; i += 1) { + const parent = name + const label = `sub${i}` + await ensRegistry.write.setSubnodeOwner([ + namehash(parent), + labelhash(label), + accounts[0].address, + ]) + name = `${label}.${name}` + } + + await publicResolver.write.setAddr([ + namehash('test.eth'), + accounts[0].address, + ]) + await publicResolver.write.setText([namehash('test.eth'), 'foo', 'bar']) + await reverseRegistrar.write.claim([accounts[0].address]) + await ensRegistry.write.setResolver([ + getReverseNodeHash(accounts[0].address), + publicResolver.address, + ]) + await publicResolver.write.setName([ + getReverseNodeHash(accounts[0].address), + 'test.eth', + ]) + await reverseRegistrar.write.claim([accounts[10].address], { + account: accounts[10], + }) + await ensRegistry.write.setResolver( + [getReverseNodeHash(accounts[10].address), oldResolver.address], + { account: accounts[10] }, + ) + const batchGatewayAbi = await hre.artifacts + .readArtifact('BatchGateway2') + .then(({ abi }) => abi) + + // OP reverse resolver (evm) + const opReverseLabel = accounts[1].address.slice(2).toLowerCase() + const opReverseNamespace = `${opHexCoinType}.reverse` + const opReverseNode = namehash(opReverseNamespace) + await ensRegistry.write.setSubnodeOwner([ + namehash('reverse'), + labelhash(opHexCoinType), + accounts[0].address, + ]) + await ensRegistry.write.setSubnodeRecord([ + opReverseNode, + labelhash(opReverseLabel), + accounts[0].address, + publicResolver.address, + 0n, + ]) + await ensRegistry.write.setSubnodeRecord([ + namehash('eth'), + labelhash('op-user'), + accounts[0].address, + publicResolver.address, + 0n, + ]) + await publicResolver.write.setName([ + namehash(`${opReverseLabel}.${opReverseNamespace}`), + 'op-user.eth', + ]) + await publicResolver.write.setAddr([ + namehash('op-user.eth'), + BigInt(opCoinType), + accounts[1].address, + ]) + + // SOL reverse resolver (non-evm) + const solReverseLabel = solAddressHex.slice(2).toLowerCase() + const solReverseNamespace = `${solHexCoinType}.reverse` + const solReverseNode = namehash(solReverseNamespace) + console.log({ solReverseNamespace, solReverseLabel }) + await ensRegistry.write.setSubnodeOwner([ + namehash('reverse'), + labelhash(solHexCoinType), + accounts[0].address, + ]) + await ensRegistry.write.setSubnodeRecord([ + solReverseNode, + labelhash(solReverseLabel), + accounts[0].address, + publicResolver.address, + 0n, + ]) + await ensRegistry.write.setSubnodeRecord([ + namehash('eth'), + labelhash('sol-user'), + accounts[0].address, + publicResolver.address, + 0n, + ]) + await publicResolver.write.setName([ + namehash(`${solReverseLabel}.${solReverseNamespace}`), + 'sol-user.eth', + ]) + await publicResolver.write.setAddr([ + namehash('sol-user.eth'), + BigInt(solCoinType), + solAddressHex, + ]) + + return { + ensRegistry, + nameWrapper, + reverseRegistrar, + publicResolver, + universalResolver, + offchainResolver, + addrOffchainResolver, + oldResolver, + revertResolver, + legacyResolver, + accounts, + batchGatewayAbi, + root, + } +} + +describe('UniversalResolver3', () => { + describe('resolve()', () => { + it('should resolve a record via legacy methods', async () => { + const { universalResolver, publicResolver, accounts } = await loadFixture( + fixture, + ) + + const args = [namehash('test.eth')] as [Hex] + + const data = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args, + }) + + const [result] = (await universalResolver.read.resolve([ + dnsEncodeName('test.eth'), + data, + ])) as ReadContractReturnType< + (typeof universalResolver)['abi'], + 'resolve', + [Hex, Hex] + > + + const decodedAddress = decodeFunctionResult< + (typeof publicResolver)['abi'], + 'addr', + typeof args + >({ + abi: publicResolver.abi, + functionName: 'addr', + data: result, + args: [namehash('test.eth')], + }) + expect(decodedAddress).toEqualAddress(accounts[0].address) + }) + + it('should throw if a resolver is not set on the queried name', async () => { + const { universalResolver, publicResolver } = await loadFixture(fixture) + + const data = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args: [namehash('no-resolver.test.other')], + }) + + await expect(universalResolver) + .read('resolve', [dnsEncodeName('no-resolver.test.other'), data]) + .toBeRevertedWithCustomError('ResolverNotFound') + }) + + it('should throw if a resolver is not a contract', async () => { + const { universalResolver, publicResolver } = await loadFixture(fixture) + + const data = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args: [namehash('non-contract-resolver.test.eth')], + }) + + await expect(universalResolver) + .read('resolve', [ + dnsEncodeName('non-contract-resolver.test.eth'), + data, + ]) + .toBeRevertedWithCustomError('ResolverNotContract') + }) + + it('should throw with revert data if resolver reverts', async () => { + const { universalResolver, publicResolver } = await loadFixture(fixture) + + const data = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args: [namehash('revert-resolver.test.eth')], + }) + + const notSupportedError = encodeErrorResult({ + abi: parseAbi(['error Error(string)']), + errorName: 'Error', + args: ['Not Supported'], + }) + + await expect(universalResolver) + .read('resolve', [dnsEncodeName('revert-resolver.test.eth'), data]) + .toBeRevertedWithCustomError('ResolverError') + .withArgs(notSupportedError) + }) + + it('should throw if a resolver is not set on the queried name, and the found resolver does not support resolve()', async () => { + const { universalResolver, publicResolver } = await loadFixture(fixture) + + const data = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args: [namehash('no-resolver.test.eth')], + }) + + await expect(universalResolver) + .read('resolve', [dnsEncodeName('no-resolver.test.eth'), data]) + .toBeRevertedWithCustomError('ResolverNotFound') + }) + + it('should resolve a record if supportsInterface() throws', async () => { + const { universalResolver, publicResolver, legacyResolver } = + await loadFixture(fixture) + + const args = [namehash('legacy-resolver.eth')] as [Hex] + + const data = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args, + }) + + const [result, resolverAddress] = (await universalResolver.read.resolve([ + dnsEncodeName('legacy-resolver.eth'), + data, + ])) as ReadContractReturnType< + (typeof universalResolver)['abi'], + 'resolve', + [Hex, Hex] + > + + const decodedAddress = decodeFunctionResult< + (typeof publicResolver)['abi'], + 'addr', + typeof args + >({ + abi: publicResolver.abi, + functionName: 'addr', + data: result, + args, + }) + expect(decodedAddress).toEqualAddress(legacyResolver.address) + expect(resolverAddress).toEqualAddress(legacyResolver.address) + }) + + it('should not run out of gas if calling a non-existent function on a legacy resolver', async () => { + const { universalResolver, publicResolver, legacyResolver } = + await loadFixture(fixture) + + const args = [namehash('legacy-resolver.eth'), 60n] as [Hex, bigint] + + const data = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args, + }) + + await expect(universalResolver) + .read('resolve', [dnsEncodeName('legacy-resolver.eth'), data]) + .toBeRevertedWithCustomError('ResolverError') + .withArgs('0x') + }) + + it('should revert with a resolveMulticall OffchainLookup when the resolver is an extended resolver and reverts with OffchainLookup', async () => { + const { universalResolver, publicResolver, offchainResolver } = + await loadFixture(fixture) + + const callData = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args: [namehash('offchain.test.eth')], + }) + + const multicallCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'multicall', + args: [[callData]], + }) + + const singleCallExtraData = encodeExtraData({ + resolverAddress: offchainResolver.address, + ...baseResolveMulticallExtraData, + internalExtraData: encodeResolveMulticallExtraData({ + name: dnsEncodeName('offchain.test.eth'), + calls: [callData], + resolverAddress: offchainResolver.address, + gateways: ['http://universal-offchain-resolver.local'], + isSingleInternallyEncodedCall: true, + }), + externalExtraData: multicallCalldata, + }) + + await expect(universalResolver) + .read('resolve', [dnsEncodeName('offchain.test.eth'), callData]) + .toBeRevertedWithCustomError('OffchainLookup') + .withArgs( + getAddress(universalResolver.address), + ['https://example.com/'], + multicallCalldata, + toFunctionSelector('function callback(bytes,bytes)'), + singleCallExtraData, + ) + }) + + it('should revert with a internalMulticall OffchainLookup when the resolver is not an extended resolver and reverts with OffchainLookup', async () => { + const { + universalResolver, + publicResolver, + addrOffchainResolver, + batchGatewayAbi, + } = await loadFixture(fixture) + + const callData = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args: [namehash('addr-offchain.test.eth')], + }) + + const fnRevertData = encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'addr', + result: addrOffchainResolver.address, + }) + + const fnRevertDataWithRewrite = encodeFunctionData({ + abi: batchGatewayAbi, + functionName: 'query', + args: [ + addrOffchainResolver.address, + ['https://example.com/'], + fnRevertData, + ], + }) + + const singleCallExtraData = encodeExtraData({ + resolverAddress: universalResolver.address, + ...baseInternalMulticallExtraData, + internalExtraData: encodeInternalMulticallExtraData({ + resolverAddress: addrOffchainResolver.address, + isSingleInternallyEncodedCall: true, + isExtendedResolver: false, + }), + externalExtraData: encodeMulticallExtraData({ + calls: [ + { + data: encodeExtraData({ + resolverAddress: addrOffchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function addrCallback(bytes,bytes)', + ), + externalExtraData: fnRevertData, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + ], + urls: ['http://universal-offchain-resolver.local'], + }), + }) + + const multicallCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'multicall', + args: [[fnRevertDataWithRewrite]], + }) + + await expect(universalResolver) + .read('resolve', [dnsEncodeName('addr-offchain.test.eth'), callData]) + .toBeRevertedWithCustomError('OffchainLookup') + .withArgs( + getAddress(universalResolver.address), + ['http://universal-offchain-resolver.local'], + multicallCalldata, + toFunctionSelector('function callback(bytes,bytes)'), + singleCallExtraData, + ) + }) + + it('should use custom gateways when specified - resolve multicall', async () => { + const { universalResolver, publicResolver, offchainResolver } = + await loadFixture(fixture) + + const callData = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args: [namehash('offchain.test.eth')], + }) + + const multicallCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'multicall', + args: [[callData]], + }) + + const singleCallExtraData = encodeExtraData({ + resolverAddress: offchainResolver.address, + ...baseResolveMulticallExtraData, + internalExtraData: encodeResolveMulticallExtraData({ + name: dnsEncodeName('offchain.test.eth'), + calls: [callData], + resolverAddress: offchainResolver.address, + gateways: ['http://custom.local'], + isSingleInternallyEncodedCall: true, + }), + externalExtraData: multicallCalldata, + }) + + await expect(universalResolver) + .read('resolveWithGateways', [ + dnsEncodeName('offchain.test.eth'), + callData, + ['http://custom.local'], + ]) + .toBeRevertedWithCustomError('OffchainLookup') + .withArgs( + getAddress(universalResolver.address), + ['https://example.com/'], + multicallCalldata, + toFunctionSelector('function callback(bytes,bytes)'), + singleCallExtraData, + ) + }) + + it('should use custom gateways when specified - internal multicall', async () => { + const { + universalResolver, + publicResolver, + addrOffchainResolver, + batchGatewayAbi, + } = await loadFixture(fixture) + + const callData = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args: [namehash('addr-offchain.test.eth')], + }) + + const fnRevertData = encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'addr', + result: addrOffchainResolver.address, + }) + + const fnRevertDataWithRewrite = encodeFunctionData({ + abi: batchGatewayAbi, + functionName: 'query', + args: [ + addrOffchainResolver.address, + ['https://example.com/'], + fnRevertData, + ], + }) + + const singleCallExtraData = encodeExtraData({ + resolverAddress: universalResolver.address, + ...baseInternalMulticallExtraData, + internalExtraData: encodeInternalMulticallExtraData({ + resolverAddress: addrOffchainResolver.address, + isSingleInternallyEncodedCall: true, + isExtendedResolver: false, + }), + externalExtraData: encodeMulticallExtraData({ + calls: [ + { + data: encodeExtraData({ + resolverAddress: addrOffchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function addrCallback(bytes,bytes)', + ), + externalExtraData: fnRevertData, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + ], + urls: ['http://custom.local'], + }), + }) + + const multicallCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'multicall', + args: [[fnRevertDataWithRewrite]], + }) + + await expect(universalResolver) + .read('resolveWithGateways', [ + dnsEncodeName('addr-offchain.test.eth'), + callData, + ['http://custom.local'], + ]) + .toBeRevertedWithCustomError('OffchainLookup') + .withArgs( + getAddress(universalResolver.address), + ['http://custom.local'], + multicallCalldata, + toFunctionSelector('function callback(bytes,bytes)'), + singleCallExtraData, + ) + }) + + // it('should return a wrapped revert with resolve() wrapped calls in extraData when combining onchain and offchain', async () => { + // const { + // universalResolver, + // publicResolver, + // offchainResolver, + // batchGatewayAbi, + // } = await loadFixture(fixture) + + // const addrCall = encodeFunctionData({ + // abi: publicResolver.abi, + // functionName: 'addr', + // args: [namehash('offchain.test.eth')], + // }) + // const onchainDataCall = '0x12345678' + + // const extraData = encodeExtraData({ + // isWildcard: false, + // resolver: offchainResolver.address, + // gateways: ['http://universal-offchain-resolver.local'], + // metadata: '0x', + // extraDatas: [ + // { + // callbackFunction: toFunctionSelector( + // 'function resolveCallback(bytes,bytes)', + // ), + // data: addrCall, + // }, + // { + // callbackFunction: '0x00000000', + // data: encodeFunctionData({ + // abi: universalResolver.abi, + // functionName: 'resolve', + // args: [dnsEncodeName('offchain.test.eth'), onchainDataCall], + // }), + // }, + // ], + // }) + + // const queryCalldata = encodeFunctionData({ + // abi: batchGatewayAbi, + // functionName: 'query', + // args: [ + // [ + // { + // sender: offchainResolver.address, + // urls: ['https://example.com/'], + // callData: addrCall, + // }, + // ], + // ], + // }) + + // await expect(universalResolver) + // .read('resolve', [ + // dnsEncodeName('offchain.test.eth'), + // [addrCall, onchainDataCall], + // ]) + // .toBeRevertedWithCustomError('OffchainLookup') + // .withArgs( + // getAddress(universalResolver.address), + // ['http://universal-offchain-resolver.local'], + // queryCalldata, + // toFunctionSelector('function resolveCallback(bytes,bytes)'), + // extraData, + // ) + // }) + + it('should revert OffchainLookup via UniversalResolver + OffchainDNSResolver', async () => { + const { universalResolver, publicResolver, ensRegistry, root } = + await loadFixture(fixture) + + const OFFCHAIN_DNS_GATEWAY = 'https://localhost:8000/lookup' + + const dnssec = await hre.viem.deployContract('DNSSECImpl', [ + encodedRealAnchors, + ]) + const suffixes = await hre.viem.deployContract( + 'SimplePublicSuffixList', + [], + ) + const dnsGatewayAbi = await hre.artifacts + .readArtifact('IDNSGateway') + .then(({ abi }) => abi) + + await suffixes.write.addPublicSuffixes([[dnsEncodeName('test')]]) + + const offchainDnsResolver = await hre.viem.deployContract( + 'OffchainDNSResolver', + [ensRegistry.address, dnssec.address, OFFCHAIN_DNS_GATEWAY], + ) + const dnsRegistrar = await hre.viem.deployContract('DNSRegistrar', [ + zeroAddress, // Previous registrar + offchainDnsResolver.address, + dnssec.address, + suffixes.address, + ensRegistry.address, + ]) + + await root.write.setController([dnsRegistrar.address, true]) + + await dnsRegistrar.write.enableNode([dnsEncodeName('test')]) + + const name = 'test.test' + + const addrCall = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args: [namehash(name)], + }) + const innerCalldata = encodeFunctionData({ + abi: dnsGatewayAbi, + functionName: 'resolve', + args: [dnsEncodeName(name), 16], + }) + const multicallCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'multicall', + args: [[addrCall]], + }) + const extraData = encodeExtraData({ + resolverAddress: offchainDnsResolver.address, + ...baseResolveMulticallExtraData, + internalExtraData: encodeResolveMulticallExtraData({ + name: dnsEncodeName(name), + calls: [addrCall], + resolverAddress: offchainDnsResolver.address, + gateways: ['http://universal-offchain-resolver.local'], + isSingleInternallyEncodedCall: true, + }), + externalExtraData: encodeAbiParameters( + parseAbiParameters('bytes, bytes, bytes4'), + [dnsEncodeName(name), multicallCalldata, '0x00000000'], + ), + }) + + await expect(universalResolver) + .read('resolve', [dnsEncodeName(name), addrCall]) + .toBeRevertedWithCustomError('OffchainLookup') + .withArgs( + getAddress(universalResolver.address), + [OFFCHAIN_DNS_GATEWAY], + innerCalldata, + toFunctionSelector('function callback(bytes,bytes)'), + extraData, + ) + }) + }) + + describe('batch', () => { + it('should resolve multiple records onchain', async () => { + const { universalResolver, publicResolver, accounts } = await loadFixture( + fixture, + ) + const addrArgs = [namehash('test.eth')] as [Hex] + const textData = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'text', + args: [namehash('test.eth'), 'foo'], + }) + const addrData = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args: addrArgs, + }) + + const [multicallResult] = await universalResolver.read.resolve([ + dnsEncodeName('test.eth'), + encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'multicall', + args: [[textData, addrData]], + }), + ]) + const [textResultEncoded, addrResultEncoded] = decodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'multicall', + data: multicallResult, + }) + + const textResult = decodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'text', + data: textResultEncoded, + args: [namehash('test.eth'), 'foo'], + }) + const addrResult = decodeFunctionResult< + (typeof publicResolver)['abi'], + 'addr', + typeof addrArgs + >({ + abi: publicResolver.abi, + functionName: 'addr', + data: addrResultEncoded, + args: addrArgs, + }) + + expect(textResult).toEqual('bar') + expect(addrResult).toEqualAddress(accounts[0].address) + }) + + it('should resolve multiple records offchain - resolve multicall', async () => { + const { universalResolver, publicResolver, offchainResolver } = + await loadFixture(fixture) + + const addrCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args: [namehash('offchain.test.eth')], + }) + const textCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'text', + args: [namehash('offchain.test.eth'), 'foo'], + }) + + const multicallCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'multicall', + args: [[addrCalldata, textCalldata]], + }) + + const extraData = encodeExtraData({ + resolverAddress: offchainResolver.address, + ...baseResolveMulticallExtraData, + internalExtraData: encodeResolveMulticallExtraData({ + name: dnsEncodeName('offchain.test.eth'), + calls: [addrCalldata, textCalldata], + resolverAddress: offchainResolver.address, + gateways: ['http://universal-offchain-resolver.local'], + isSingleInternallyEncodedCall: false, + }), + externalExtraData: multicallCalldata, + }) + + await expect(universalResolver) + .read('resolve', [ + dnsEncodeName('offchain.test.eth'), + multicallCalldata, + ]) + .toBeRevertedWithCustomError('OffchainLookup') + .withArgs( + getAddress(universalResolver.address), + ['https://example.com/'], + multicallCalldata, + toFunctionSelector('function callback(bytes,bytes)'), + extraData, + ) + }) + + it('should resolve multiple records offchain - internal multicall', async () => { + const { + universalResolver, + publicResolver, + addrOffchainResolver, + batchGatewayAbi, + } = await loadFixture(fixture) + + const addrCalldata1 = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args: [namehash('addr-offchain.test.eth')], + }) + + const addrCalldata2 = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args: [namehash('addr-offchain.test.eth')], + }) + + const multicallCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'multicall', + args: [[addrCalldata1, addrCalldata2]], + }) + + const fnRevertData = encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'addr', + result: addrOffchainResolver.address, + }) + + const fnRevertDataWithRewrite = encodeFunctionData({ + abi: batchGatewayAbi, + functionName: 'query', + args: [ + addrOffchainResolver.address, + ['https://example.com/'], + fnRevertData, + ], + }) + + const extraData = encodeExtraData({ + resolverAddress: universalResolver.address, + ...baseInternalMulticallExtraData, + internalExtraData: encodeInternalMulticallExtraData({ + resolverAddress: addrOffchainResolver.address, + isSingleInternallyEncodedCall: false, + isExtendedResolver: false, + }), + externalExtraData: encodeMulticallExtraData({ + calls: [ + { + data: encodeExtraData({ + resolverAddress: addrOffchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function addrCallback(bytes,bytes)', + ), + externalExtraData: fnRevertData, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + { + data: encodeExtraData({ + resolverAddress: addrOffchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function addrCallback(bytes,bytes)', + ), + externalExtraData: fnRevertData, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + ], + urls: ['http://universal-offchain-resolver.local'], + }), + }) + + const multicallRevertCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'multicall', + args: [[fnRevertDataWithRewrite, fnRevertDataWithRewrite]], + }) + + await expect(universalResolver) + .read('resolve', [ + dnsEncodeName('addr-offchain.test.eth'), + multicallCalldata, + ]) + .toBeRevertedWithCustomError('OffchainLookup') + .withArgs( + getAddress(universalResolver.address), + ['http://universal-offchain-resolver.local'], + multicallRevertCalldata, + toFunctionSelector('function callback(bytes,bytes)'), + extraData, + ) + }) + + it('should handle mixed offchain/onchain in internal multicall', async () => { + const { + universalResolver, + publicResolver, + addrOffchainResolver, + batchGatewayAbi, + } = await loadFixture(fixture) + + const addrCalldata1 = encodeFunctionData({ + abi: addrOffchainResolver.abi, + functionName: 'addr', + args: [namehash('addr-offchain.test.eth')], + }) + + const addrCalldataOnchain = encodeFunctionData({ + abi: addrOffchainResolver.abi, + functionName: 'addrOnchain', + args: [namehash('addr-offchain.test.eth')], + }) + + const multicallCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'multicall', + args: [[addrCalldata1, addrCalldataOnchain]], + }) + + const fnRevertData = encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'addr', + result: addrOffchainResolver.address, + }) + + const fnRevertDataWithRewrite = encodeFunctionData({ + abi: batchGatewayAbi, + functionName: 'query', + args: [ + addrOffchainResolver.address, + ['https://example.com/'], + fnRevertData, + ], + }) + + const extraData = encodeExtraData({ + resolverAddress: universalResolver.address, + ...baseInternalMulticallExtraData, + internalExtraData: encodeInternalMulticallExtraData({ + resolverAddress: addrOffchainResolver.address, + isSingleInternallyEncodedCall: false, + isExtendedResolver: false, + }), + externalExtraData: encodeMulticallExtraData({ + calls: [ + { + data: encodeExtraData({ + resolverAddress: addrOffchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function addrCallback(bytes,bytes)', + ), + externalExtraData: fnRevertData, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + { + data: fnRevertData, + offchain: false, + }, + ], + urls: ['http://universal-offchain-resolver.local'], + }), + }) + + const multicallRevertCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'multicall', + args: [[fnRevertDataWithRewrite]], + }) + + await expect(universalResolver) + .read('resolve', [ + dnsEncodeName('addr-offchain.test.eth'), + multicallCalldata, + ]) + .toBeRevertedWithCustomError('OffchainLookup') + .withArgs( + getAddress(universalResolver.address), + ['http://universal-offchain-resolver.local'], + multicallRevertCalldata, + toFunctionSelector('function callback(bytes,bytes)'), + extraData, + ) + }) + }) + + describe('resolveMulticallResolveCallback()', () => { + // TODO: should resolve a single record (single internally encoded call) + it('should resolve a single record (single internally encoded call)', async () => { + const { universalResolver, publicResolver, offchainResolver } = + await loadFixture(fixture) + + const addrArgs = [namehash('offchain.test.eth')] as [Hex] + const callData = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args: addrArgs, + }) + + const multicallCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'multicall', + args: [[callData]], + }) + + const response = encodeAbiParameters(parseAbiParameters('bytes[]'), [ + [ + encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'addr', + result: offchainResolver.address, + }), + ], + ]) + + const singleCallExtraData = encodeExtraData({ + resolverAddress: offchainResolver.address, + ...baseResolveMulticallExtraData, + internalExtraData: encodeResolveMulticallExtraData({ + name: dnsEncodeName('offchain.test.eth'), + calls: [callData], + resolverAddress: offchainResolver.address, + gateways: ['http://universal-offchain-resolver.local'], + isSingleInternallyEncodedCall: true, + }), + externalExtraData: multicallCalldata, + }) + + const publicClient = await hre.viem.getPublicClient() + const [encodedAddrResult, resolverAddress] = + await publicClient.readContract({ + abi: parseAbi([ + 'function callback(bytes, bytes) view returns (bytes,address)', + ]), + functionName: 'callback', + address: universalResolver.address, + args: [response, singleCallExtraData], + }) + + const decodedAddrResult = decodeFunctionResult< + (typeof publicResolver)['abi'], + 'addr', + typeof addrArgs + >({ + abi: publicResolver.abi, + functionName: 'addr', + data: encodedAddrResult, + args: addrArgs, + }) + + expect(decodedAddrResult).toEqualAddress(offchainResolver.address) + expect(resolverAddress).toEqualAddress(offchainResolver.address) + }) + it('should resolve multiple records', async () => { + const { universalResolver, publicResolver, offchainResolver } = + await loadFixture(fixture) + + const addrArgs = [namehash('offchain.test.eth')] as [Hex] + const addrCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args: addrArgs, + }) + const textCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'text', + args: [namehash('offchain.test.eth'), 'foo'], + }) + + const multicallCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'multicall', + args: [[addrCalldata, textCalldata]], + }) + + const response = encodeAbiParameters(parseAbiParameters('bytes[]'), [ + [ + encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'addr', + result: offchainResolver.address, + }), + encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'text', + result: 'bar', + }), + ], + ]) + + const singleCallExtraData = encodeExtraData({ + resolverAddress: offchainResolver.address, + ...baseResolveMulticallExtraData, + internalExtraData: encodeResolveMulticallExtraData({ + name: dnsEncodeName('offchain.test.eth'), + calls: [addrCalldata, textCalldata], + resolverAddress: offchainResolver.address, + gateways: ['http://universal-offchain-resolver.local'], + isSingleInternallyEncodedCall: false, + }), + externalExtraData: multicallCalldata, + }) + + const publicClient = await hre.viem.getPublicClient() + const [multicallResult, resolverAddress] = + await publicClient.readContract({ + abi: parseAbi([ + 'function callback(bytes, bytes) view returns (bytes,address)', + ]), + functionName: 'callback', + address: universalResolver.address, + args: [response, singleCallExtraData], + }) + const [encodedAddrResult, encodedTextResult] = decodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'multicall', + data: multicallResult, + }) + + const decodedAddrResult = decodeFunctionResult< + (typeof publicResolver)['abi'], + 'addr', + typeof addrArgs + >({ + abi: publicResolver.abi, + functionName: 'addr', + data: encodedAddrResult, + args: addrArgs, + }) + + const decodedTextResult = decodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'text', + data: encodedTextResult, + }) + + expect(decodedAddrResult).toEqualAddress(offchainResolver.address) + expect(decodedTextResult).toEqual('bar') + expect(resolverAddress).toEqualAddress(offchainResolver.address) + }) + // TODO: should resolve multiple records + // TODO: should handle empty/error response with internal multicall + it('should handle error response with internal multicall', async () => { + const { + universalResolver, + publicResolver, + offchainResolver, + batchGatewayAbi, + } = await loadFixture(fixture) + + const callData = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'contenthash', + args: [namehash('offchain.test.eth')], + }) + + const originalMulticallCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'multicall', + args: [[callData]], + }) + + const response = encodeAbiParameters(parseAbiParameters('bytes[]'), [ + [ + encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'contenthash', + result: '0x1234', + }), + ], + ]) + + const resolveMulticallExtraData = encodeExtraData({ + resolverAddress: offchainResolver.address, + ...baseResolveMulticallExtraData, + internalExtraData: encodeResolveMulticallExtraData({ + name: dnsEncodeName('offchain.test.eth'), + calls: [callData], + resolverAddress: offchainResolver.address, + gateways: ['http://universal-offchain-resolver.local'], + isSingleInternallyEncodedCall: true, + }), + externalExtraData: originalMulticallCalldata, + }) + + const fnRevertDataWithRewrite = encodeFunctionData({ + abi: batchGatewayAbi, + functionName: 'query', + args: [offchainResolver.address, ['https://example.com/'], callData], + }) + + const revertExtraData = encodeExtraData({ + resolverAddress: universalResolver.address, + ...baseInternalMulticallExtraData, + internalExtraData: encodeInternalMulticallExtraData({ + resolverAddress: offchainResolver.address, + isSingleInternallyEncodedCall: true, + isExtendedResolver: true, + }), + externalExtraData: encodeMulticallExtraData({ + calls: [ + { + data: encodeExtraData({ + resolverAddress: offchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function resolveCallback(bytes,bytes)', + ), + externalExtraData: callData, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + ], + urls: ['http://universal-offchain-resolver.local'], + }), + }) + + const multicallCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'multicall', + args: [[fnRevertDataWithRewrite]], + }) + + await expect(universalResolver) + .read('callback', [response, resolveMulticallExtraData]) + .toBeRevertedWithCustomError('OffchainLookup') + .withArgs( + getAddress(universalResolver.address), + ['http://universal-offchain-resolver.local'], + multicallCalldata, + toFunctionSelector('function callback(bytes,bytes)'), + revertExtraData, + ) + }) + it('should handle empty response with internal multicall', async () => { + const { + universalResolver, + publicResolver, + offchainResolver, + batchGatewayAbi, + } = await loadFixture(fixture) + + const callData = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'name', + args: [namehash('offchain.test.eth')], + }) + + const originalMulticallCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'multicall', + args: [[callData]], + }) + + const response = encodeAbiParameters(parseAbiParameters('bytes[]'), [ + [ + encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'name', + result: 'idk', + }), + ], + ]) + + const resolveMulticallExtraData = encodeExtraData({ + resolverAddress: offchainResolver.address, + ...baseResolveMulticallExtraData, + internalExtraData: encodeResolveMulticallExtraData({ + name: dnsEncodeName('offchain.test.eth'), + calls: [callData], + resolverAddress: offchainResolver.address, + gateways: ['http://universal-offchain-resolver.local'], + isSingleInternallyEncodedCall: true, + }), + externalExtraData: originalMulticallCalldata, + }) + + const fnRevertDataWithRewrite = encodeFunctionData({ + abi: batchGatewayAbi, + functionName: 'query', + args: [offchainResolver.address, ['https://example.com/'], callData], + }) + + const revertExtraData = encodeExtraData({ + resolverAddress: universalResolver.address, + ...baseInternalMulticallExtraData, + internalExtraData: encodeInternalMulticallExtraData({ + resolverAddress: offchainResolver.address, + isSingleInternallyEncodedCall: true, + isExtendedResolver: true, + }), + externalExtraData: encodeMulticallExtraData({ + calls: [ + { + data: encodeExtraData({ + resolverAddress: offchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function resolveCallback(bytes,bytes)', + ), + externalExtraData: callData, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + ], + urls: ['http://universal-offchain-resolver.local'], + }), + }) + + const multicallCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'multicall', + args: [[fnRevertDataWithRewrite]], + }) + + await expect(universalResolver) + .read('callback', [response, resolveMulticallExtraData]) + .toBeRevertedWithCustomError('OffchainLookup') + .withArgs( + getAddress(universalResolver.address), + ['http://universal-offchain-resolver.local'], + multicallCalldata, + toFunctionSelector('function callback(bytes,bytes)'), + revertExtraData, + ) + }) + // TODO: should propagate empty response error when single internally encoded call + it('should propagate empty response error when single internally encoded call', async () => { + const { + universalResolver, + publicResolver, + offchainResolver, + batchGatewayAbi, + } = await loadFixture(fixture) + + const callData = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'pubkey', + args: [namehash('offchain.test.eth')], + }) + + const originalMulticallCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'multicall', + args: [[callData]], + }) + + const response = encodeAbiParameters(parseAbiParameters('bytes[]'), [ + [ + encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'pubkey', + result: [zeroHash, zeroHash], + }), + ], + ]) + + const resolveMulticallExtraData = encodeExtraData({ + resolverAddress: offchainResolver.address, + ...baseResolveMulticallExtraData, + internalExtraData: encodeResolveMulticallExtraData({ + name: dnsEncodeName('offchain.test.eth'), + calls: [callData], + resolverAddress: offchainResolver.address, + gateways: ['http://universal-offchain-resolver.local'], + isSingleInternallyEncodedCall: true, + }), + externalExtraData: originalMulticallCalldata, + }) + + await expect(universalResolver) + .read('callback', [response, resolveMulticallExtraData]) + .toBeRevertedWithCustomError('ResolverError') + .withArgs('0x') + }) + }) + + describe('internalMulticallResolveCallback()', () => { + // TODO: should resolve a single record (single internally encoded call) + it('should resolve a single record (single internally encoded call) - standard resolver', async () => { + const { + universalResolver, + publicResolver, + addrOffchainResolver, + accounts, + } = await loadFixture(fixture) + + const fnRevertData = encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'addr', + result: accounts[0].address, + }) + + const singleCallExtraData = encodeExtraData({ + resolverAddress: universalResolver.address, + ...baseInternalMulticallExtraData, + internalExtraData: encodeInternalMulticallExtraData({ + resolverAddress: addrOffchainResolver.address, + isSingleInternallyEncodedCall: true, + isExtendedResolver: false, + }), + externalExtraData: encodeMulticallExtraData({ + calls: [ + { + data: encodeExtraData({ + resolverAddress: addrOffchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function addrCallback(bytes,bytes)', + ), + externalExtraData: fnRevertData, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + ], + urls: ['http://universal-offchain-resolver.local'], + }), + }) + + const multicallServerResponse = encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'multicall', + result: [[fnRevertData]] as unknown as [Hex], + }) + + const publicClient = await hre.viem.getPublicClient() + const [encodedAddrResult, resolverAddress] = + await publicClient.readContract({ + abi: parseAbi([ + 'function callback(bytes, bytes) view returns (bytes,address)', + ]), + functionName: 'callback', + address: universalResolver.address, + args: [multicallServerResponse, singleCallExtraData], + }) + expect(resolverAddress).toEqualAddress(addrOffchainResolver.address) + + const decodedAddrResult = decodeFunctionResult< + (typeof publicResolver)['abi'], + 'addr', + [Hex] + >({ + abi: publicResolver.abi, + functionName: 'addr', + data: encodedAddrResult, + args: [namehash('addr-offchain.test.eth')], + }) + expect(decodedAddrResult).toEqualAddress(accounts[0].address) + }) + it('should resolve a single record (single internally encoded call) - extended resolver', async () => { + const { universalResolver, publicResolver, offchainResolver, accounts } = + await loadFixture(fixture) + + const fnRevertData = encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'addr', + result: accounts[0].address, + }) + + const singleCallExtraData = encodeExtraData({ + resolverAddress: universalResolver.address, + ...baseInternalMulticallExtraData, + internalExtraData: encodeInternalMulticallExtraData({ + resolverAddress: offchainResolver.address, + isSingleInternallyEncodedCall: true, + isExtendedResolver: true, + }), + externalExtraData: encodeMulticallExtraData({ + calls: [ + { + data: encodeExtraData({ + resolverAddress: offchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function resolveCallback(bytes,bytes)', + ), + externalExtraData: fnRevertData, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + ], + urls: ['http://universal-offchain-resolver.local'], + }), + }) + + const multicallServerResponse = encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'multicall', + result: [[fnRevertData]] as unknown as [Hex], + }) + + const publicClient = await hre.viem.getPublicClient() + const [encodedAddrResult, resolverAddress] = + await publicClient.readContract({ + abi: parseAbi([ + 'function callback(bytes, bytes) view returns (bytes,address)', + ]), + functionName: 'callback', + address: universalResolver.address, + args: [multicallServerResponse, singleCallExtraData], + }) + expect(resolverAddress).toEqualAddress(offchainResolver.address) + + const decodedAddrResult = decodeFunctionResult< + (typeof publicResolver)['abi'], + 'addr', + [Hex] + >({ + abi: publicResolver.abi, + functionName: 'addr', + data: encodedAddrResult, + args: [namehash('offchain.test.eth')], + }) + expect(decodedAddrResult).toEqualAddress(accounts[0].address) + }) + it('should resolve multiple records - standard resolver', async () => { + const { + universalResolver, + publicResolver, + addrOffchainResolver, + accounts, + } = await loadFixture(fixture) + + const response1 = encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'addr', + result: accounts[0].address, + }) + const response2 = encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'addr', + result: accounts[1].address, + }) + + const serverMulticallResponse = encodeAbiParameters( + parseAbiParameters('bytes[]'), + [[response1, response2]], + ) + + const singleCallExtraData = encodeExtraData({ + resolverAddress: universalResolver.address, + ...baseInternalMulticallExtraData, + internalExtraData: encodeInternalMulticallExtraData({ + resolverAddress: addrOffchainResolver.address, + isSingleInternallyEncodedCall: false, + isExtendedResolver: false, + }), + externalExtraData: encodeMulticallExtraData({ + calls: [ + { + data: encodeExtraData({ + resolverAddress: addrOffchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function addrCallback(bytes,bytes)', + ), + externalExtraData: response1, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + { + data: encodeExtraData({ + resolverAddress: addrOffchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function addrCallback(bytes,bytes)', + ), + externalExtraData: response2, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + ], + urls: ['http://universal-offchain-resolver.local'], + }), + }) + + const publicClient = await hre.viem.getPublicClient() + const [multicallResult, resolverAddress] = + await publicClient.readContract({ + abi: parseAbi([ + 'function callback(bytes, bytes) view returns (bytes,address)', + ]), + functionName: 'callback', + address: universalResolver.address, + args: [serverMulticallResponse, singleCallExtraData], + }) + expect(resolverAddress).toEqualAddress(addrOffchainResolver.address) + + const [encodedAddrResult1, encodedAddrResult2] = decodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'multicall', + data: multicallResult, + }) + const decodedAddrResult1 = decodeFunctionResult< + (typeof publicResolver)['abi'], + 'addr', + [Hex] + >({ + abi: publicResolver.abi, + functionName: 'addr', + data: encodedAddrResult1, + args: [namehash('addr-offchain.test.eth')], + }) + expect(decodedAddrResult1).toEqualAddress(accounts[0].address) + const decodedAddrResult2 = decodeFunctionResult< + (typeof publicResolver)['abi'], + 'addr', + [Hex] + >({ + abi: publicResolver.abi, + functionName: 'addr', + data: encodedAddrResult2, + args: [namehash('addr-offchain.test.eth')], + }) + expect(decodedAddrResult2).toEqualAddress(accounts[1].address) + }) + it('should resolve multiple records - extended resolver', async () => { + const { universalResolver, publicResolver, offchainResolver, accounts } = + await loadFixture(fixture) + + const addrArgs = [namehash('offchain.test.eth')] as [Hex] + const addrCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args: addrArgs, + }) + + const response = encodeAbiParameters(parseAbiParameters('bytes[]'), [ + [ + encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'addr', + result: accounts[0].address, + }), + encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'addr', + result: accounts[1].address, + }), + ], + ]) + + const singleCallExtraData = encodeExtraData({ + resolverAddress: universalResolver.address, + ...baseInternalMulticallExtraData, + internalExtraData: encodeInternalMulticallExtraData({ + resolverAddress: offchainResolver.address, + isSingleInternallyEncodedCall: false, + isExtendedResolver: true, + }), + externalExtraData: encodeMulticallExtraData({ + calls: [ + { + data: encodeExtraData({ + resolverAddress: offchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function resolveCallback(bytes,bytes)', + ), + externalExtraData: addrCalldata, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + { + data: encodeExtraData({ + resolverAddress: offchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function resolveCallback(bytes,bytes)', + ), + externalExtraData: addrCalldata, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + ], + urls: ['http://universal-offchain-resolver.local'], + }), + }) + + const publicClient = await hre.viem.getPublicClient() + const [multicallResult, resolverAddress] = + await publicClient.readContract({ + abi: parseAbi([ + 'function callback(bytes, bytes) view returns (bytes,address)', + ]), + functionName: 'callback', + address: universalResolver.address, + args: [response, singleCallExtraData], + }) + expect(resolverAddress).toEqualAddress(offchainResolver.address) + + const [encodedAddrResult1, encodedAddrResult2] = decodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'multicall', + data: multicallResult, + }) + + const decodedAddrResult1 = decodeFunctionResult< + (typeof publicResolver)['abi'], + 'addr', + typeof addrArgs + >({ + abi: publicResolver.abi, + functionName: 'addr', + data: encodedAddrResult1, + args: addrArgs, + }) + expect(decodedAddrResult1).toEqualAddress(accounts[0].address) + const decodedAddrResult2 = decodeFunctionResult< + (typeof publicResolver)['abi'], + 'addr', + typeof addrArgs + >({ + abi: publicResolver.abi, + functionName: 'addr', + data: encodedAddrResult2, + args: addrArgs, + }) + expect(decodedAddrResult2).toEqualAddress(accounts[1].address) + }) + it('should handle mixed offchain/onchain', async () => { + const { + universalResolver, + publicResolver, + addrOffchainResolver, + accounts, + } = await loadFixture(fixture) + + const addrArgs = [namehash('addr-offchain.test.eth')] as [Hex] + const addrCalldata1 = encodeFunctionData({ + abi: addrOffchainResolver.abi, + functionName: 'addr', + args: addrArgs, + }) + + const addrOffchainResponse = encodeFunctionResult({ + abi: addrOffchainResolver.abi, + functionName: 'addr', + result: accounts[0].address, + }) + + const addrOnchainResponse = encodeFunctionResult({ + abi: addrOffchainResolver.abi, + functionName: 'addrOnchain', + result: addrOffchainResolver.address, + }) + + const serverMulticallResponse = encodeAbiParameters( + parseAbiParameters('bytes[]'), + [[addrOffchainResponse]], + ) + + const extraData = encodeExtraData({ + resolverAddress: universalResolver.address, + ...baseInternalMulticallExtraData, + internalExtraData: encodeInternalMulticallExtraData({ + resolverAddress: addrOffchainResolver.address, + isSingleInternallyEncodedCall: false, + isExtendedResolver: false, + }), + externalExtraData: encodeMulticallExtraData({ + calls: [ + { + data: encodeExtraData({ + resolverAddress: addrOffchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function addrCallback(bytes,bytes)', + ), + externalExtraData: addrCalldata1, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + { + data: addrOnchainResponse, + offchain: false, + }, + ], + urls: ['http://universal-offchain-resolver.local'], + }), + }) + + const publicClient = await hre.viem.getPublicClient() + const [multicallResult, resolverAddress] = + await publicClient.readContract({ + abi: parseAbi([ + 'function callback(bytes, bytes) view returns (bytes,address)', + ]), + functionName: 'callback', + address: universalResolver.address, + args: [serverMulticallResponse, extraData], + }) + expect(resolverAddress).toEqualAddress(addrOffchainResolver.address) + + const [encodedAddrResultOffchain, encodedAddrResultOnchain] = + decodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'multicall', + data: multicallResult, + }) + + const decodedAddrResultOffchain = decodeFunctionResult< + (typeof publicResolver)['abi'], + 'addr', + typeof addrArgs + >({ + abi: publicResolver.abi, + functionName: 'addr', + data: encodedAddrResultOffchain, + args: addrArgs, + }) + expect(decodedAddrResultOffchain).toEqualAddress(accounts[0].address) + const decodedAddrResultOnchain = decodeFunctionResult< + (typeof publicResolver)['abi'], + 'addr', + typeof addrArgs + >({ + abi: publicResolver.abi, + functionName: 'addr', + data: encodedAddrResultOnchain, + args: addrArgs, + }) + expect(decodedAddrResultOnchain).toEqualAddress( + addrOffchainResolver.address, + ) + }) + it('should propagate empty response error when single internally encoded call', async () => { + const { universalResolver, publicResolver, offchainResolver } = + await loadFixture(fixture) + + const callData = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'pubkey', + args: [namehash('offchain.test.eth')], + }) + + const response = encodeAbiParameters(parseAbiParameters('bytes[]'), [ + [ + encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'pubkey', + result: [zeroHash, zeroHash], + }), + ], + ]) + + const extraData = encodeExtraData({ + resolverAddress: universalResolver.address, + ...baseInternalMulticallExtraData, + internalExtraData: encodeInternalMulticallExtraData({ + resolverAddress: offchainResolver.address, + isSingleInternallyEncodedCall: true, + isExtendedResolver: true, + }), + externalExtraData: encodeMulticallExtraData({ + calls: [ + { + data: encodeExtraData({ + resolverAddress: offchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function resolveCallback(bytes,bytes)', + ), + externalExtraData: callData, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + ], + urls: ['http://universal-offchain-resolver.local'], + }), + }) + + await expect(universalResolver) + .read('callback', [response, extraData]) + .toBeRevertedWithCustomError('ResolverError') + .withArgs('0x') + }) + it('should propagate error response when single internally encoded call', async () => { + const { universalResolver, publicResolver, offchainResolver } = + await loadFixture(fixture) + + const callData = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'contenthash', + args: [namehash('offchain.test.eth')], + }) + + const response = encodeAbiParameters(parseAbiParameters('bytes[]'), [ + [ + encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'contenthash', + result: zeroHash, + }), + ], + ]) + + const extraData = encodeExtraData({ + resolverAddress: universalResolver.address, + ...baseInternalMulticallExtraData, + internalExtraData: encodeInternalMulticallExtraData({ + resolverAddress: offchainResolver.address, + isSingleInternallyEncodedCall: true, + isExtendedResolver: true, + }), + externalExtraData: encodeMulticallExtraData({ + calls: [ + { + data: encodeExtraData({ + resolverAddress: offchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function resolveCallback(bytes,bytes)', + ), + externalExtraData: callData, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + ], + urls: ['http://universal-offchain-resolver.local'], + }), + }) + + await expect(universalResolver) + .read('callback', [response, extraData]) + .toBeRevertedWithCustomError('ResolverError') + .withArgs( + encodeErrorResult({ + abi: parseAbi(['error Error(string)']), + errorName: 'Error', + args: ['Unsupported call'], + }), + ) + }) + it('should propagate HttpError when single internally encoded call', async () => { + const { universalResolver, publicResolver, offchainResolver } = + await loadFixture(fixture) + + const callData = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args: [namehash('offchain.test.eth')], + }) + + const httpError = encodeErrorResult({ + abi: parseAbi(['error HttpError(uint16 status, string message)']), + errorName: 'HttpError', + args: [404, 'Not Found'], + }) + + const response = encodeAbiParameters(parseAbiParameters('bytes[]'), [ + [httpError], + ]) + + const extraData = encodeExtraData({ + resolverAddress: universalResolver.address, + ...baseInternalMulticallExtraData, + internalExtraData: encodeInternalMulticallExtraData({ + resolverAddress: offchainResolver.address, + isSingleInternallyEncodedCall: true, + isExtendedResolver: true, + }), + externalExtraData: encodeMulticallExtraData({ + calls: [ + { + data: encodeExtraData({ + resolverAddress: offchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function resolveCallback(bytes,bytes)', + ), + externalExtraData: callData, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + ], + urls: ['http://universal-offchain-resolver.local'], + }), + }) + + await expect(universalResolver) + .read('callback', [response, extraData]) + .toBeRevertedWithCustomError('HttpError') + .withArgs(404, 'Not Found') + }) + it('should not revert if there is an error in a call', async () => { + const { universalResolver, publicResolver, offchainResolver, accounts } = + await loadFixture(fixture) + + const addrArgs = [namehash('offchain.test.eth')] as [Hex] + const addrCalldata = encodeFunctionData({ + abi: publicResolver.abi, + functionName: 'addr', + args: addrArgs, + }) + + const httpError = encodeErrorResult({ + abi: parseAbi(['error HttpError(uint16 status, string message)']), + errorName: 'HttpError', + args: [404, 'Not Found'], + }) + + const response = encodeAbiParameters(parseAbiParameters('bytes[]'), [ + [ + httpError, + encodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'addr', + result: accounts[1].address, + }), + ], + ]) + + const singleCallExtraData = encodeExtraData({ + resolverAddress: universalResolver.address, + ...baseInternalMulticallExtraData, + internalExtraData: encodeInternalMulticallExtraData({ + resolverAddress: offchainResolver.address, + isSingleInternallyEncodedCall: false, + isExtendedResolver: true, + }), + externalExtraData: encodeMulticallExtraData({ + calls: [ + { + data: encodeExtraData({ + resolverAddress: offchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function resolveCallback(bytes,bytes)', + ), + externalExtraData: addrCalldata, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + { + data: encodeExtraData({ + resolverAddress: offchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function resolveCallback(bytes,bytes)', + ), + externalExtraData: addrCalldata, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + ], + urls: ['http://universal-offchain-resolver.local'], + }), + }) + + const publicClient = await hre.viem.getPublicClient() + const [multicallResult, resolverAddress] = + await publicClient.readContract({ + abi: parseAbi([ + 'function callback(bytes, bytes) view returns (bytes,address)', + ]), + functionName: 'callback', + address: universalResolver.address, + args: [response, singleCallExtraData], + }) + expect(resolverAddress).toEqualAddress(offchainResolver.address) + + const [errorResult, encodedAddrResult] = decodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'multicall', + data: multicallResult, + }) + + expect(errorResult).toMatchObject(httpError) + const decodedAddrResult = decodeFunctionResult< + (typeof publicResolver)['abi'], + 'addr', + typeof addrArgs + >({ + abi: publicResolver.abi, + functionName: 'addr', + data: encodedAddrResult, + args: addrArgs, + }) + expect(decodedAddrResult).toEqualAddress(accounts[1].address) + }) + + it('should allow first offchain call at non-0 index', async () => { + const { + universalResolver, + publicResolver, + addrOffchainResolver, + accounts, + } = await loadFixture(fixture) + + const addrArgs = [namehash('addr-offchain.test.eth')] as [Hex] + const addrCalldata1 = encodeFunctionData({ + abi: addrOffchainResolver.abi, + functionName: 'addr', + args: addrArgs, + }) + + const addrOffchainResponse = encodeFunctionResult({ + abi: addrOffchainResolver.abi, + functionName: 'addr', + result: accounts[0].address, + }) + + const addrOnchainResponse = encodeFunctionResult({ + abi: addrOffchainResolver.abi, + functionName: 'addrOnchain', + result: addrOffchainResolver.address, + }) + + const serverMulticallResponse = encodeAbiParameters( + parseAbiParameters('bytes[]'), + [[addrOffchainResponse]], + ) + + const extraData = encodeExtraData({ + resolverAddress: universalResolver.address, + ...baseInternalMulticallExtraData, + internalExtraData: encodeInternalMulticallExtraData({ + resolverAddress: addrOffchainResolver.address, + isSingleInternallyEncodedCall: false, + isExtendedResolver: false, + }), + externalExtraData: encodeMulticallExtraData({ + calls: [ + { + data: addrOnchainResponse, + offchain: false, + }, + { + data: encodeExtraData({ + resolverAddress: addrOffchainResolver.address, + ...baseInternalCallExtraData, + externalCallbackFunction: toFunctionSelector( + 'function addrCallback(bytes,bytes)', + ), + externalExtraData: addrCalldata1, + }), + callbackFunction: toFunctionSelector( + 'function callback(bytes,bytes)', + ), + offchain: true, + }, + ], + urls: ['http://universal-offchain-resolver.local'], + }), + }) + + const publicClient = await hre.viem.getPublicClient() + const [multicallResult, resolverAddress] = + await publicClient.readContract({ + abi: parseAbi([ + 'function callback(bytes, bytes) view returns (bytes,address)', + ]), + functionName: 'callback', + address: universalResolver.address, + args: [serverMulticallResponse, extraData], + }) + expect(resolverAddress).toEqualAddress(addrOffchainResolver.address) + + const [encodedAddrResultOnchain, encodedAddrResultOffchain] = + decodeFunctionResult({ + abi: publicResolver.abi, + functionName: 'multicall', + data: multicallResult, + }) + + const decodedAddrResultOffchain = decodeFunctionResult< + (typeof publicResolver)['abi'], + 'addr', + typeof addrArgs + >({ + abi: publicResolver.abi, + functionName: 'addr', + data: encodedAddrResultOffchain, + args: addrArgs, + }) + expect(decodedAddrResultOffchain).toEqualAddress(accounts[0].address) + const decodedAddrResultOnchain = decodeFunctionResult< + (typeof publicResolver)['abi'], + 'addr', + typeof addrArgs + >({ + abi: publicResolver.abi, + functionName: 'addr', + data: encodedAddrResultOnchain, + args: addrArgs, + }) + expect(decodedAddrResultOnchain).toEqualAddress( + addrOffchainResolver.address, + ) + }) + }) + + describe('reverse()', () => { + // TODO: should resolve with onchain resolution + it('should resolve eth', async () => { + const { universalResolver, publicResolver, accounts } = await loadFixture( + fixture, + ) + + const [name, resolver, reverseResolver] = + await universalResolver.read.reverse([accounts[0].address, 60n]) + expect(name).toEqual('test.eth') + expect(resolver).toEqualAddress(publicResolver.address) + expect(reverseResolver).toEqualAddress(publicResolver.address) + }) + // TODO: should fallback to resolving eth via legacy addr(bytes32) if addr(bytes32,uint256) fails + it('should fallback to resolving eth via legacy addr(bytes32) when addr(bytes32,uint256) fails', async () => { + const { universalResolver, oldResolver, accounts } = await loadFixture( + fixture, + ) + const [name, resolver, reverseResolver] = + await universalResolver.read.reverse([accounts[10].address, 60n]) + expect(name).toEqual('oldprimary.eth') + expect(resolver).toEqualAddress(oldResolver.address) + expect(reverseResolver).toEqualAddress(oldResolver.address) + }) + // TODO: should resolve evm chains + it('should resolve evm chains', async () => { + const { universalResolver, publicResolver, accounts } = await loadFixture( + fixture, + ) + + const [name, resolver, reverseResolver] = + await universalResolver.read.reverse([ + accounts[1].address, + BigInt(opCoinType), + ]) + expect(name).toEqual('op-user.eth') + expect(resolver).toEqualAddress(publicResolver.address) + expect(reverseResolver).toEqualAddress(publicResolver.address) + }) + // TODO: should resolve non-evm chains + it('should resolve non-evm chains', async () => { + const { universalResolver, publicResolver, accounts } = await loadFixture( + fixture, + ) + const resolverData = await universalResolver.read.findResolver([ + dnsEncodeName(`${solAddressHex.slice(2)}.${solHexCoinType}.reverse`), + ]) + console.log( + dnsEncodeName(`${solAddressHex.slice(2)}.${solHexCoinType}.reverse`), + ) + console.log(resolverData) + const [name, resolver, reverseResolver] = + await universalResolver.read.reverse([ + solAddressHex, + BigInt(solCoinType), + ]) + expect(name).toEqual('sol-user.eth') + expect(resolver).toEqualAddress(publicResolver.address) + expect(reverseResolver).toEqualAddress(publicResolver.address) + }) + // TODO: should use provided gateways + // TODO: should propagate HttpError + // TODO: should revert with mismatching evm chain address + // TODO: should revert with mismatching non-evm chain address + }) + + // describe('reverseCallback', () => { + // it('should revert with metadata for initial forward resolution if required', async () => { + // const { universalResolver, offchainResolver, batchGatewayAbi } = + // await loadFixture(fixture) + + // const metadata = encodeAbiParameters( + // [{ type: 'string' }, { type: 'address' }], + // ['offchain.test.eth', offchainResolver.address], + // ) + // const addrCall = encodeFunctionData({ + // abi: offchainResolver.abi, + // functionName: 'addr', + // args: [namehash('offchain.test.eth')], + // }) + + // const extraData = encodeExtraData({ + // isWildcard: false, + // resolver: offchainResolver.address, + // gateways: ['http://universal-offchain-resolver.local'], + // metadata: '0x', + // extraDatas: [ + // { + // callbackFunction: toFunctionSelector( + // 'function resolveCallback(bytes,bytes)', + // ), + // data: '0x691f3431', + // }, + // ], + // }) + // const extraDataForResult = encodeExtraData({ + // isWildcard: false, + // resolver: offchainResolver.address, + // gateways: ['http://universal-offchain-resolver.local'], + // metadata, + // extraDatas: [ + // { + // callbackFunction: toFunctionSelector( + // 'function resolveCallback(bytes,bytes)', + // ), + // data: addrCall, + // }, + // ], + // }) + // const response = encodeFunctionResult({ + // abi: batchGatewayAbi, + // functionName: 'query', + // result: [[false], ['0x691f3431']], + // }) + + // await expect(universalResolver) + // .read('reverseCallback', [response, extraData]) + // .toBeRevertedWithCustomError('OffchainLookup') + // .withArgs( + // getAddress(universalResolver.address), + // ['http://universal-offchain-resolver.local'], + // expect.anyValue, + // toFunctionSelector('function reverseCallback(bytes,bytes)'), + // extraDataForResult, + // ) + // }) + + // it('should resolve address record via a callback from offchain lookup', async () => { + // const { universalResolver, offchainResolver, batchGatewayAbi } = + // await loadFixture(fixture) + + // const metadata = encodeAbiParameters( + // [{ type: 'string' }, { type: 'address' }], + // ['offchain.test.eth', offchainResolver.address], + // ) + // const extraData = encodeExtraData({ + // isWildcard: false, + // resolver: offchainResolver.address, + // gateways: ['http://universal-offchain-resolver.local'], + // metadata, + // extraDatas: [ + // { + // callbackFunction: toFunctionSelector( + // 'function resolveCallback(bytes,bytes)', + // ), + // data: '0x', + // }, + // ], + // }) + // const response = encodeFunctionResult({ + // abi: batchGatewayAbi, + // functionName: 'query', + // result: [[false], ['0x']], + // }) + + // const [name, a1, a2, a3] = await universalResolver.read.reverseCallback([ + // response, + // extraData, + // ]) + + // expect(name).toEqual('offchain.test.eth') + // expect(a1).toEqualAddress(offchainResolver.address) + // expect(a2).toEqualAddress(offchainResolver.address) + // expect(a3).toEqualAddress(offchainResolver.address) + // }) + + // it('should propagate HttpError', async () => { + // const { universalResolver, offchainResolver, batchGatewayAbi } = + // await loadFixture(fixture) + + // const publicClient = await hre.viem.getPublicClient() + + // const universalResolverWithHttpError = getContract({ + // abi: [ + // ...universalResolver.abi, + // parseAbiItem('error HttpError((uint16,string)[])'), + // ], + // address: universalResolver.address, + // client: publicClient, + // }) + + // const errorData = encodeErrorResult({ + // abi: universalResolverWithHttpError.abi, + // errorName: 'HttpError', + // args: [[[404, 'Not Found']]], + // }) + + // const metadata = encodeAbiParameters( + // [{ type: 'string' }, { type: 'address' }], + // ['offchain.test.eth', offchainResolver.address], + // ) + // const extraData = encodeExtraData({ + // isWildcard: false, + // resolver: offchainResolver.address, + // gateways: ['http://universal-offchain-resolver.local'], + // metadata, + // extraDatas: [ + // { + // callbackFunction: toFunctionSelector( + // 'function resolveCallback(bytes,bytes)', + // ), + // data: errorData, + // }, + // ], + // }) + // const response = encodeFunctionResult({ + // abi: batchGatewayAbi, + // functionName: 'query', + // result: [[true], [errorData]], + // }) + + // await expect(universalResolverWithHttpError) + // .read('reverseCallback', [response, extraData]) + // .toBeRevertedWithCustomError('HttpError') + // .withArgs([[404, 'Not Found']]) + // }) + // }) + + // describe('reverse()', () => { + // it('should resolve a reverse record with name and resolver address', async () => { + // const { universalResolver, accounts, publicResolver } = await loadFixture( + // fixture, + // ) + + // const [name, resolvedAddress, reverseResolverAddress, resolverAddress] = + // (await universalResolver.read.reverse([ + // dnsEncodeName(getReverseNode(accounts[0].address)), + // ])) as ReadContractReturnType< + // (typeof universalResolver)['abi'], + // 'reverse', + // [Hex] + // > + + // expect(name).toEqual('test.eth') + // expect(resolvedAddress).toEqualAddress(accounts[1].address) + // expect(reverseResolverAddress).toEqualAddress(publicResolver.address) + // expect(resolverAddress).toEqualAddress(publicResolver.address) + // }) + + // it('should not use all the gas on a internal resolver revert', async () => { + // const { universalResolver, accounts } = await loadFixture(fixture) + + // await expect(universalResolver) + // .read('reverse', [dnsEncodeName(getReverseNode(accounts[10].address))]) + // .not.toBeReverted() + // }) + // }) +})