Skip to content

Commit

Permalink
Merge pull request #324 from ensdomains/fix/ur-hybrid-calls
Browse files Browse the repository at this point in the history
fix: universalresolver hybrid onchain/offchain handling
  • Loading branch information
TateB authored Feb 19, 2024
2 parents fac78e2 + 4b222f8 commit 021ba7d
Show file tree
Hide file tree
Showing 10 changed files with 524 additions and 97 deletions.
6 changes: 2 additions & 4 deletions contracts/utils/UniversalResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,7 @@ contract UniversalResolver is ERC165, Ownable {

Result memory result = results[0];

if (!result.success) {
revert ResolverError(result.returnData);
}
_checkResolveSingle(result);

if (metaData.length > 0) {
(string memory resolvedName, address reverseResolverAddress) = abi
Expand Down Expand Up @@ -658,7 +656,7 @@ contract UniversalResolver is ERC165, Ownable {
returnData = abi.decode(returnData, (bytes));
}
results[i] = Result(success, returnData);
extraDatas[i].data = multicallData.data[i];
extraDatas[i].data = item;
}

if (offchainCount == 0) {
Expand Down
10 changes: 6 additions & 4 deletions deploy/utils/00_deploy_universal_resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
log: true,
})

const UR = await ethers.getContract('UniversalResolver')
const tx = await UR.transferOwnership(owner)
console.log(`Transfer ownership to ${owner} (tx: ${tx.hash})...`)
await tx.wait()
if (owner !== undefined && owner !== deployer) {
const UR = await ethers.getContract('UniversalResolver')
const tx = await UR.transferOwnership(owner)
console.log(`Transfer ownership to ${owner} (tx: ${tx.hash})...`)
await tx.wait()
}
}

func.id = 'universal-resolver'
Expand Down
46 changes: 23 additions & 23 deletions deployments/goerli/UniversalResolver.json

Large diffs are not rendered by default.

101 changes: 101 additions & 0 deletions deployments/goerli/solcInputs/49f758ec505ff69b72f3179ac11d7cfc.json

Large diffs are not rendered by default.

58 changes: 29 additions & 29 deletions deployments/holesky/UniversalResolver.json

Large diffs are not rendered by default.

101 changes: 101 additions & 0 deletions deployments/holesky/solcInputs/49f758ec505ff69b72f3179ac11d7cfc.json

Large diffs are not rendered by default.

54 changes: 27 additions & 27 deletions deployments/sepolia/UniversalResolver.json

Large diffs are not rendered by default.

101 changes: 101 additions & 0 deletions deployments/sepolia/solcInputs/49f758ec505ff69b72f3179ac11d7cfc.json

Large diffs are not rendered by default.

135 changes: 127 additions & 8 deletions test/utils/TestUniversalResolver.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { solidity } = require('ethereum-waffle')
const { use, expect } = require('chai')
const namehash = require('eth-ens-namehash')
const { hexDataSlice } = require('ethers/lib/utils')
const { hexDataSlice, concat } = require('ethers/lib/utils')
const sha3 = require('web3-utils').sha3
const { Contract } = require('ethers')
const { ethers } = require('hardhat')
Expand Down Expand Up @@ -194,6 +194,12 @@ contract('UniversalResolver', function (accounts) {
4,
)

const resolveSig = ethers.utils.hexDataSlice(
ethers.utils.id('resolve(bytes,bytes)'),
0,
4,
)

describe('findResolver()', () => {
it('should find an exact match resolver', async () => {
const result = await universalResolver.findResolver(
Expand Down Expand Up @@ -451,6 +457,63 @@ contract('UniversalResolver', function (accounts) {
}
})

it('should return a wrapped revert with resolve() wrapped calls in extraData when combining onchain and offchain lookups', async () => {
const addrData = publicResolver.interface.encodeFunctionData(
'addr(bytes32)',
[namehash.hash('offchain.test.eth')],
)
const onchainDataCall = '0x12345678'

try {
await universalResolver['resolve(bytes,bytes[])'](
dns.hexEncodeName('offchain.test.eth'),
[addrData, onchainDataCall],
)
expect(false).to.be.true
} catch (e) {
expect(e.errorName).to.equal('OffchainLookup')
expect(e.errorArgs.sender).to.equal(universalResolver.address)
expect(e.errorArgs.urls).to.deep.equal([
'http://universal-offchain-resolver.local/',
])
const decodedCallData = batchGateway.decodeFunctionData(
'query',
e.errorArgs.callData,
)
expect(decodedCallData).to.deep.equal([
[[dummyOffchainResolver.address, ['https://example.com/'], addrData]],
])
expect(e.errorArgs.callbackFunction).to.equal(
ethers.utils.hexDataSlice(
ethers.utils.id('resolveCallback(bytes,bytes)'),
0,
4,
),
)
const decodedExtraData = ethers.utils.defaultAbiCoder.decode(
['bool', 'address', 'string[]', 'bytes', '(bytes4,bytes)[]'],
e.errorArgs.extraData,
)
expect(decodedExtraData).to.deep.equal([
false,
dummyOffchainResolver.address,
['http://universal-offchain-resolver.local/'],
'0x',
[
[resolveCallbackSig, addrData],
[
'0x00000000',
// just using the UR interface for ensip10
universalResolver.interface.encodeFunctionData(
'resolve(bytes,bytes)',
[dns.hexEncodeName('offchain.test.eth'), onchainDataCall],
),
],
],
])
}
})

describe('batch', () => {
it('should resolve multiple records onchain', async () => {
const textData = publicResolver.interface.encodeFunctionData(
Expand Down Expand Up @@ -515,7 +578,24 @@ contract('UniversalResolver', function (accounts) {
expect(false).to.be.true
} catch (e) {
expect(e.errorName).to.equal('OffchainLookup')
expect(e.errorArgs.callData).to.equal(callData)
const decodedCallData = batchGateway.decodeFunctionData(
'query',
e.errorArgs.callData,
)
expect(decodedCallData).to.deep.equal([
[
[
dummyOffchainResolver.address,
['https://example.com/'],
textData,
],
[
dummyOffchainResolver.address,
['https://example.com/'],
addrData,
],
],
])
expect(e.errorArgs.callbackFunction).to.equal(resolveCallbackSig)
expect(e.errorArgs.extraData).to.equal(extraData)
}
Expand Down Expand Up @@ -661,9 +741,9 @@ contract('UniversalResolver', function (accounts) {
expect(encodedRes.returnData).to.equal('0x')
})
it('should allow response at non-0 extraData index', async () => {
const addrData = publicResolver.interface.encodeFunctionData(
'addr(bytes32)',
[namehash.hash('offchain.test.eth')],
const onchainCall = universalResolver.interface.encodeFunctionData(
'resolve(bytes,bytes)',
[dns.hexEncodeName('offchain.test.eth'), '0x12345678'],
)
const textData = publicResolver.interface.encodeFunctionData(
'text(bytes32,string)',
Expand All @@ -677,7 +757,7 @@ contract('UniversalResolver', function (accounts) {
['http://universal-offchain-resolver.local/'],
'0x',
[
['0x00000000', addrData],
['0x00000000', onchainCall],
[resolveCallbackSig, textData],
],
],
Expand All @@ -690,15 +770,15 @@ contract('UniversalResolver', function (accounts) {
await universalResolver.callStatic.resolveCallback(responses, extraData)
expect(encodedRes.success).to.equal(true)
expect(encodedResTwo.success).to.equal(true)
const [addrRet] = ethers.utils.defaultAbiCoder.decode(
const [fooString] = ethers.utils.defaultAbiCoder.decode(
['bytes'],
encodedRes.returnData,
)
const [addrRetTwo] = publicResolver.interface.decodeFunctionResult(
'addr(bytes32)',
encodedResTwo.returnData,
)
expect(ethers.utils.toUtf8String(addrRet)).to.equal('onchain')
expect(ethers.utils.toUtf8String(fooString)).to.equal('foo')
expect(addrRetTwo).to.equal(dummyOffchainResolver.address)
expect(resolverAddress).to.equal(dummyOffchainResolver.address)
})
Expand Down Expand Up @@ -801,6 +881,45 @@ contract('UniversalResolver', function (accounts) {
expect(a2).to.equal(dummyOffchainResolver.address)
expect(a3).to.equal(dummyOffchainResolver.address)
})
it('should propagate HttpError', async () => {
const urWithHttpErrorAbi = new ethers.Contract(
universalResolver.address,
[
...universalResolver.interface.fragments,
'error HttpError((uint16,string)[])',
],
ethers.provider,
)
const errorData = urWithHttpErrorAbi.interface.encodeErrorResult(
'HttpError',
[[[404, 'Not Found']]],
)
const extraData = ethers.utils.defaultAbiCoder.encode(
['bool', 'address', 'string[]', 'bytes', '(bytes4,bytes)[]'],
[
false,
dummyOffchainResolver.address,
['http://universal-offchain-resolver.local/'],
'0x',
[[resolveCallbackSig, errorData]],
],
)
const responses = batchGateway.encodeFunctionResult('query', [
[true],
[errorData],
])

try {
await urWithHttpErrorAbi.callStatic.reverseCallback(
responses,
extraData,
)
expect(false).to.be.true
} catch (e) {
expect(e.errorName).to.equal('HttpError')
expect(e.errorArgs).to.deep.equal([[[404, 'Not Found']]])
}
})
})

describe('reverse()', () => {
Expand Down
9 changes: 7 additions & 2 deletions test/utils/mocks/DummyOffchainResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "../../../contracts/resolvers/profiles/ITextResolver.sol";
import "../../../contracts/resolvers/profiles/IExtendedResolver.sol";

error OffchainLookup(
Expand All @@ -27,6 +28,10 @@ contract DummyOffchainResolver is IExtendedResolver, ERC165 {
) external view returns (bytes memory) {
string[] memory urls = new string[](1);
urls[0] = "https://example.com/";

if (bytes4(data) == bytes4(0x12345678)) {
return abi.encode("foo");
}
revert OffchainLookup(
address(this),
urls,
Expand All @@ -36,8 +41,8 @@ contract DummyOffchainResolver is IExtendedResolver, ERC165 {
);
}

function addr(bytes32) external pure returns (bytes memory) {
return abi.encode("onchain");
function addr(bytes32) external pure returns (address) {
return 0x69420f05A11f617B4B74fFe2E04B2D300dFA556F;
}

function resolveCallback(
Expand Down

0 comments on commit 021ba7d

Please sign in to comment.