From f8f38209ebc2d4e3e8013f8271ba44109d93dd99 Mon Sep 17 00:00:00 2001 From: Andrew Raffensperger Date: Sun, 28 Dec 2025 10:06:00 -0800 Subject: [PATCH 1/3] wip --- src/IOffchainVerifier.sol | 17 +++++++++++++ src/OffchainResolver.sol | 50 ++++++++------------------------------- src/OffchainVerifier.sol | 39 ++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 40 deletions(-) create mode 100755 src/IOffchainVerifier.sol create mode 100755 src/OffchainVerifier.sol diff --git a/src/IOffchainVerifier.sol b/src/IOffchainVerifier.sol new file mode 100755 index 0000000..31eb494 --- /dev/null +++ b/src/IOffchainVerifier.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +interface IOffchainVerifierSigner { + function isTrustedSigner(address) external view returns (bool); +} + +interface IOffchainVerifier { + error CCIPReadExpired(uint64 expiry); + error CCIPReadUntrusted(address signed); + + /// @notice Verify `response` was signed by `IOffchainVerifierSigner(msg.sender).isTrustedSigner()`. + function verifyResponse( + bytes calldata request, + bytes calldata response + ) external view returns (bytes memory); +} diff --git a/src/OffchainResolver.sol b/src/OffchainResolver.sol index 089663d..d1220a5 100755 --- a/src/OffchainResolver.sol +++ b/src/OffchainResolver.sol @@ -3,26 +3,24 @@ pragma solidity >=0.8.13; import {Ownable} from "@oz/access/Ownable.sol"; import {ERC165} from "@oz/utils/introspection/ERC165.sol"; -import {ECDSA} from "@oz/utils/cryptography/ECDSA.sol"; import {IERC7996} from "@ens/utils/IERC7996.sol"; import {ResolverFeatures} from "@ens/resolvers/ResolverFeatures.sol"; import {IExtendedResolver} from "@ens/resolvers/profiles/IExtendedResolver.sol"; +import {IVerifiableResolver} from "@ens/resolvers/profiles/IVerifiableResolver.sol"; import {OffchainLookup} from "@ens/ccipRead/EIP3668.sol"; -import {IGatewayProvider} from "@ens/ccipRead/IGatewayProvider.sol"; contract OffchainResolver is Ownable, ERC165, IExtendedResolver, - IGatewayProvider, + IVerifiableResolver, IERC7996 { - error CCIPReadExpired(uint64 expiry); - error CCIPReadUntrusted(address signed); event SignerChanged(address signer, bool enabled); event GatewaysChanged(string[] gateways); + IOffchainVerifier _verifier; string[] _gateways; /// @notice Determine if `signer` is a trusted signer. @@ -48,8 +46,8 @@ contract OffchainResolver is ) public view override returns (bool) { return interfaceId == type(IExtendedResolver).interfaceId || + interfaceId == type(IVerifiableResolver).interfaceId || interfaceId == type(IERC7996).interfaceId || - interfaceId == type(IGatewayProvider).interfaceId || super.supportsInterface(interfaceId); } @@ -60,6 +58,11 @@ contract OffchainResolver is return featureId == ResolverFeatures.RESOLVE_MULTICALL; } + /// @inheritdoc IVerifiableResolver + function verifierMetadata(bytes) external view returns (address verifier, string[] memory gateways) { + return (_verifier, _gateways); + } + /// @notice Set `signer` as an trusted signer. function setSigner(address signer, bool enabled) external onlyOwner { require(isSigner[signer] != enabled); @@ -73,11 +76,6 @@ contract OffchainResolver is emit GatewaysChanged(gateways_); } - /// @inheritdoc IGatewayProvider - function gateways() external view returns (string[] memory) { - return _gateways; - } - /// @inheritdoc IExtendedResolver function resolve( bytes calldata /*name*/, @@ -97,35 +95,7 @@ contract OffchainResolver is bytes calldata response, bytes calldata request ) external view returns (bytes memory) { - return _verifyResponse(request, response); + return _verifier.verifyResponse(request, response); } - /// @dev Verify `signedResponse` was signed by `signer`. - function _verifyResponse( - bytes memory request, - bytes calldata response - ) internal view returns (bytes memory) { - (bytes memory answer, uint64 expiry, bytes memory sig) = abi.decode( - response, - (bytes, uint64, bytes) - ); - if (expiry < block.timestamp) { - revert CCIPReadExpired(expiry); - } - // standard "ens" offchain signing protocol - bytes32 hash = keccak256( - abi.encodePacked( - hex"1900", - address(this), - expiry, - keccak256(request), // original calldata, eg. msg.data - keccak256(answer) // response from server - ) - ); - address signed = ECDSA.recover(hash, sig); - if (!isSigner[signed]) { - revert CCIPReadUntrusted(signed); - } - return answer; - } } diff --git a/src/OffchainVerifier.sol b/src/OffchainVerifier.sol new file mode 100755 index 0000000..a02d461 --- /dev/null +++ b/src/OffchainVerifier.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +import {ERC165} from "@oz/utils/introspection/ERC165.sol"; +import {ECDSA} from "@oz/utils/cryptography/ECDSA.sol"; +import { + IOffchainVerifier, + IOffchainVerifierSigner +} from "./IOffchainVerifier.sol"; + +contract OffchainVerifier is IOffchainVerifier { + function verifyResponse( + bytes memory request, + bytes calldata response + ) internal view returns (bytes memory) { + (bytes memory answer, uint64 expiry, bytes memory sig) = abi.decode( + response, + (bytes, uint64, bytes) + ); + if (expiry < block.timestamp) { + revert CCIPReadExpired(expiry); + } + // standard "ens" offchain signing protocol + bytes32 hash = keccak256( + abi.encodePacked( + hex"1900", + address(msg.sender), + expiry, + keccak256(request), // original calldata, eg. msg.data + keccak256(answer) // response from server + ) + ); + address signed = ECDSA.recover(hash, sig); + if (!IOffchainVerifierSigner(msg.sender).isTrusedSigner(signed)) { + revert CCIPReadUntrusted(signed); + } + return answer; + } +} From ddb9b5268e5a8ae29eef911ab07abf91e799adb9 Mon Sep 17 00:00:00 2001 From: Andrew Raffensperger Date: Sun, 28 Dec 2025 14:22:22 -0500 Subject: [PATCH 2/3] working external verifier --- bun.lock | 1 + src/IOffchainVerifier.sol | 4 +- src/OffchainResolver.sol | 32 ++++--- src/OffchainVerifier.sol | 23 +++-- test/e2e.test.ts | 180 ++++++++++++++++++++------------------ 5 files changed, 134 insertions(+), 106 deletions(-) diff --git a/bun.lock b/bun.lock index b1001ed..51a8f15 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "offchain-resolver-example", diff --git a/src/IOffchainVerifier.sol b/src/IOffchainVerifier.sol index 31eb494..c7e84d3 100755 --- a/src/IOffchainVerifier.sol +++ b/src/IOffchainVerifier.sol @@ -2,14 +2,14 @@ pragma solidity >=0.8.13; interface IOffchainVerifierSigner { - function isTrustedSigner(address) external view returns (bool); + function isOffchainSigner(address) external view returns (bool); } interface IOffchainVerifier { error CCIPReadExpired(uint64 expiry); error CCIPReadUntrusted(address signed); - /// @notice Verify `response` was signed by `IOffchainVerifierSigner(msg.sender).isTrustedSigner()`. + /// @notice Verify `response` was signed by `IOffchainVerifierSigner(msg.sender).isOffchainSigner()`. function verifyResponse( bytes calldata request, bytes calldata response diff --git a/src/OffchainResolver.sol b/src/OffchainResolver.sol index d1220a5..645a06e 100755 --- a/src/OffchainResolver.sol +++ b/src/OffchainResolver.sol @@ -6,17 +6,20 @@ import {ERC165} from "@oz/utils/introspection/ERC165.sol"; import {IERC7996} from "@ens/utils/IERC7996.sol"; import {ResolverFeatures} from "@ens/resolvers/ResolverFeatures.sol"; import {IExtendedResolver} from "@ens/resolvers/profiles/IExtendedResolver.sol"; -import {IVerifiableResolver} from "@ens/resolvers/profiles/IVerifiableResolver.sol"; +import { + IVerifiableResolver +} from "@ens/resolvers/profiles/IVerifiableResolver.sol"; import {OffchainLookup} from "@ens/ccipRead/EIP3668.sol"; +import {IOffchainVerifier, IOffchainVerifierSigner} from "./IOffchainVerifier.sol"; contract OffchainResolver is Ownable, ERC165, IExtendedResolver, IVerifiableResolver, + IOffchainVerifierSigner, IERC7996 { - event SignerChanged(address signer, bool enabled); event GatewaysChanged(string[] gateways); @@ -24,20 +27,24 @@ contract OffchainResolver is string[] _gateways; /// @notice Determine if `signer` is a trusted signer. - mapping(address signer => bool enabled) public isSigner; + mapping(address signer => bool enabled) public isOffchainSigner; constructor( address owner, + IOffchainVerifier verifier, address[] memory signers, - string[] memory gateways_ + string[] memory gateways ) Ownable(owner) { + _verifier = verifier; for (uint256 i; i < signers.length; ++i) { address signer = signers[i]; - isSigner[signer] = true; + isOffchainSigner[signer] = true; emit SignerChanged(signer, true); } - _gateways = gateways_; - emit GatewaysChanged(gateways_); + if (gateways.length > 0) { + _gateways = gateways; + emit GatewaysChanged(gateways); + } } /// @inheritdoc ERC165 @@ -59,14 +66,16 @@ contract OffchainResolver is } /// @inheritdoc IVerifiableResolver - function verifierMetadata(bytes) external view returns (address verifier, string[] memory gateways) { - return (_verifier, _gateways); + function verifierMetadata( + bytes calldata /*name*/ + ) external view returns (address verifier, string[] memory gateways) { + return (address(_verifier), _gateways); } /// @notice Set `signer` as an trusted signer. function setSigner(address signer, bool enabled) external onlyOwner { - require(isSigner[signer] != enabled); - isSigner[signer] = enabled; + require(isOffchainSigner[signer] != enabled); + isOffchainSigner[signer] = enabled; emit SignerChanged(signer, enabled); } @@ -97,5 +106,4 @@ contract OffchainResolver is ) external view returns (bytes memory) { return _verifier.verifyResponse(request, response); } - } diff --git a/src/OffchainVerifier.sol b/src/OffchainVerifier.sol index a02d461..ba6400b 100755 --- a/src/OffchainVerifier.sol +++ b/src/OffchainVerifier.sol @@ -8,11 +8,21 @@ import { IOffchainVerifierSigner } from "./IOffchainVerifier.sol"; -contract OffchainVerifier is IOffchainVerifier { +contract OffchainVerifier is ERC165, IOffchainVerifier { + /// @inheritdoc ERC165 + function supportsInterface( + bytes4 interfaceId + ) public view override returns (bool) { + return + interfaceId == type(IOffchainVerifier).interfaceId || + super.supportsInterface(interfaceId); + } + + /// @inheritdoc IOffchainVerifier function verifyResponse( - bytes memory request, + bytes calldata request, bytes calldata response - ) internal view returns (bytes memory) { + ) external view returns (bytes memory) { (bytes memory answer, uint64 expiry, bytes memory sig) = abi.decode( response, (bytes, uint64, bytes) @@ -20,18 +30,19 @@ contract OffchainVerifier is IOffchainVerifier { if (expiry < block.timestamp) { revert CCIPReadExpired(expiry); } + /// forge-lint: disable-next-item(asm-keccak256) // standard "ens" offchain signing protocol bytes32 hash = keccak256( abi.encodePacked( - hex"1900", - address(msg.sender), + bytes2(0x1900), + msg.sender, expiry, keccak256(request), // original calldata, eg. msg.data keccak256(answer) // response from server ) ); address signed = ECDSA.recover(hash, sig); - if (!IOffchainVerifierSigner(msg.sender).isTrusedSigner(signed)) { + if (!IOffchainVerifierSigner(msg.sender).isOffchainSigner(signed)) { revert CCIPReadUntrusted(signed); } return answer; diff --git a/test/e2e.test.ts b/test/e2e.test.ts index b546d3b..08265a8 100755 --- a/test/e2e.test.ts +++ b/test/e2e.test.ts @@ -17,95 +17,103 @@ const ETH = "0x51050ec063d393217B436747617aD1C2285Aeeee"; const BTC = "0xdeadfee5"; const CH = "0x1234"; -describe("e2e", () => { - let F: Foundry; - let S: Awaited>; - let OR: DeployedContract; - let UR: DeployedContract; - let ENS: DeployedContract; - beforeAll(async () => { - F = await Foundry.launch({ infoLog: false }); // enable to show anvil events - // deploy OffchainResolver - const { admin } = F.wallets; - OR = await F.deploy({ - file: "OffchainResolver", - args: [admin, [], []], - }); - // setup offchain server - S = await serve( - (name) => { - if (name === "raffy.eth") { - return { - addr(coinType) { - if (coinType === 0x8000_0000n) { - return ETH; // default address - } else if (coinType === 0n) { - return BTC; - } - }, - text(key) { - return `key is ${key}`; - }, - contenthash() { - return CH; - }, - // if not a standard profile - // by default, ezccip will return UnsupportedResolverProfile() - }; - } - // we dont know this name - // by default, ezccip will return UnreachableName() - }, - { protocol: "ens" } - ); - // ezccip generates a random key if not specified - // add this key as a trusted signer - await F.confirm(OR.setSigner(S.signer, true)); - // to support recursive ccip-read, we ignore the ccip sender, - // and instead sign relative to the contract we're supporting. - // note: ezccip automatically interprets the first address - // in an URL as the "origin" of the ccip request - await F.confirm(OR.setGateways([`${S.endpoint}/${OR.target}`])); - // deploy ENS registry - ENS = await F.deploy({ - import: "@ens/registry/ENSRegistry.sol", - }); - // setup "addr.reverse" to handle ReverseClaimer.claim() on UR - const FakeReverseRegistrar = await F.deploy(`contract X { - function claim(address) external pure returns (bytes32) {} - }`); - await F.confirm(ENS.setSubnodeOwner(ZeroHash, labelhash("reverse"), admin)); - await F.confirm( - ENS.setSubnodeOwner( - namehash("reverse"), - labelhash("addr"), - FakeReverseRegistrar - ) - ); - // deploy UniversalResolver - const BatchGatewayProvider = await F.deploy({ - import: "@ens/ccipRead/GatewayProvider.sol", - args: [admin, []], // no gateways are required since OffchainResolver supports ENSIP-22 - }); - UR = await F.deploy({ - import: "@ens/universalResolver/UniversalResolver.sol", - args: [admin, ENS, BatchGatewayProvider], - }); - // setup "raffy.eth" - await F.confirm(ENS.setSubnodeOwner(ZeroHash, labelhash("eth"), admin)); - await F.confirm( - ENS.setSubnodeRecord(namehash("eth"), labelhash("raffy"), admin, OR, 0) - ); +describe("e2e", async () => { + const F = await Foundry.launch({ infoLog: false }); // enable to show anvil events + afterAll(F.shutdown); + await F.parseArtifacts(); // all interfaces + + // deploy OffchainResolver + const { admin } = F.wallets; + const OffchainVerifier = await F.deploy({ + file: "OffchainVerifier", + }); + const OR = await F.deploy({ + file: "OffchainResolver", + args: [admin, OffchainVerifier, [], []], + }); + // setup offchain server + const S = await serve( + (name) => { + if (name === "raffy.eth") { + return { + addr(coinType) { + if (coinType === 0x8000_0000n) { + return ETH; // default address + } else if (coinType === 0n) { + return BTC; + } + }, + text(key) { + return `key is ${key}`; + }, + contenthash() { + return CH; + }, + // if not a standard profile + // by default, ezccip will return UnsupportedResolverProfile() + }; + } + // we dont know this name + // by default, ezccip will return UnreachableName() + }, + { protocol: "ens" } + ); + // ezccip generates a random key if not specified + // add this key as a trusted signer + expect( + F.getEventResults( + await F.confirm(OR.setSigner(S.signer, true)), + "SignerChanged" + ) + ).toEqual<[[string, boolean]]>([[S.signer, true]]); + // to support recursive ccip-read, we ignore the ccip sender, + // and instead sign relative to the contract we're supporting. + // note: ezccip automatically interprets the first address + // in an URL as the "origin" of the ccip request + const gateways = [`${S.endpoint}/${OR.target}`]; + expect( + F.getEventResults( + await F.confirm(OR.setGateways(gateways)), + "GatewaysChanged" + ) + ).toEqual<[[string[]]]>([[gateways]]); + // deploy ENS registry + const ENS = await F.deploy({ + import: "@ens/registry/ENSRegistry.sol", + }); + // setup "addr.reverse" to handle ReverseClaimer.claim() on UR + const FakeReverseRegistrar = await F.deploy(`contract X { + function claim(address) external pure returns (bytes32) {} + }`); + await F.confirm(ENS.setSubnodeOwner(ZeroHash, labelhash("reverse"), admin)); + await F.confirm( + ENS.setSubnodeOwner( + namehash("reverse"), + labelhash("addr"), + FakeReverseRegistrar + ) + ); + // deploy UniversalResolver + const BatchGatewayProvider = await F.deploy({ + import: "@ens/ccipRead/GatewayProvider.sol", + args: [admin, []], // no gateways are required since OffchainResolver supports ENSIP-22 + }); + const UR = await F.deploy({ + import: "@ens/universalResolver/UniversalResolver.sol", + args: [admin, ENS, BatchGatewayProvider], }); - afterAll(() => F?.shutdown); - afterAll(() => S?.shutdown); + // setup "raffy.eth" + await F.confirm(ENS.setSubnodeOwner(ZeroHash, labelhash("eth"), admin)); + await F.confirm( + ENS.setSubnodeRecord(namehash("eth"), labelhash("raffy"), admin, OR, 0) + ); test("toggle signer", async () => { - expect(OR.isSigner(ETH)).resolves.toBeFalse(); + expect(OR.isOffchainSigner(ETH)).resolves.toBeFalse(); await F.confirm(OR.setSigner(ETH, true)); - expect(OR.isSigner(ETH)).resolves.toBeTrue(); + expect(OR.isOffchainSigner(ETH)).resolves.toBeTrue(); await F.confirm(OR.setSigner(ETH, false)); - expect(OR.isSigner(ETH)).resolves.toBeFalse(); + expect(OR.isOffchainSigner(ETH)).resolves.toBeFalse(); }); test("UnreachableName", async () => { @@ -285,12 +293,12 @@ describe("e2e", () => { const HR = await F.deploy({ sol: `import "@src/OffchainResolver.sol"; contract HR is OffchainResolver { - constructor(address[] memory ss) OffchainResolver(msg.sender, ss, new string[](0)) {} + constructor(address[] memory ss, IOffchainVerifier v) OffchainResolver(msg.sender, v, ss, new string[](0)) {} function supportsFeature(bytes4) external pure override returns (bool) { return false; } }`, - args: [[S.signer]], + args: [[S.signer], OffchainVerifier], }); await F.confirm(HR.setGateways([`${S.endpoint}/${HR.target}`])); await F.confirm(ENS.setResolver(namehash("raffy.eth"), HR)); // replace resolver in registry From 48de99f7f98143d84f1fd20a07e81793febdbe96 Mon Sep 17 00:00:00 2001 From: Andrew Raffensperger Date: Sun, 28 Dec 2025 15:47:51 -0500 Subject: [PATCH 3/3] external signer storage --- lib/ens-contracts | 2 +- src/IOffchainVerifier.sol | 2 +- src/OffchainResolver.sol | 42 ++++++++----------------------- src/OffchainVerifier.sol | 36 ++++++++++++++++++++++++++- test/e2e.test.ts | 52 +++++++++++++++++++-------------------- 5 files changed, 72 insertions(+), 62 deletions(-) diff --git a/lib/ens-contracts b/lib/ens-contracts index 289913d..be53b9c 160000 --- a/lib/ens-contracts +++ b/lib/ens-contracts @@ -1 +1 @@ -Subproject commit 289913d7e3923228675add09498d66920216fe9b +Subproject commit be53b9c25be5b2c7326f524bbd34a3939374ab1f diff --git a/src/IOffchainVerifier.sol b/src/IOffchainVerifier.sol index c7e84d3..e165061 100755 --- a/src/IOffchainVerifier.sol +++ b/src/IOffchainVerifier.sol @@ -9,7 +9,7 @@ interface IOffchainVerifier { error CCIPReadExpired(uint64 expiry); error CCIPReadUntrusted(address signed); - /// @notice Verify `response` was signed by `IOffchainVerifierSigner(msg.sender).isOffchainSigner()`. + /// @notice Verify `response` was signed by an authorized account. function verifyResponse( bytes calldata request, bytes calldata response diff --git a/src/OffchainResolver.sol b/src/OffchainResolver.sol index 645a06e..58048ee 100755 --- a/src/OffchainResolver.sol +++ b/src/OffchainResolver.sol @@ -10,42 +10,21 @@ import { IVerifiableResolver } from "@ens/resolvers/profiles/IVerifiableResolver.sol"; import {OffchainLookup} from "@ens/ccipRead/EIP3668.sol"; -import {IOffchainVerifier, IOffchainVerifierSigner} from "./IOffchainVerifier.sol"; +import {IOffchainVerifier} from "./IOffchainVerifier.sol"; contract OffchainResolver is Ownable, ERC165, IExtendedResolver, IVerifiableResolver, - IOffchainVerifierSigner, IERC7996 { - event SignerChanged(address signer, bool enabled); event GatewaysChanged(string[] gateways); IOffchainVerifier _verifier; string[] _gateways; - /// @notice Determine if `signer` is a trusted signer. - mapping(address signer => bool enabled) public isOffchainSigner; - - constructor( - address owner, - IOffchainVerifier verifier, - address[] memory signers, - string[] memory gateways - ) Ownable(owner) { - _verifier = verifier; - for (uint256 i; i < signers.length; ++i) { - address signer = signers[i]; - isOffchainSigner[signer] = true; - emit SignerChanged(signer, true); - } - if (gateways.length > 0) { - _gateways = gateways; - emit GatewaysChanged(gateways); - } - } + constructor(address owner) Ownable(owner) {} /// @inheritdoc ERC165 function supportsInterface( @@ -72,17 +51,16 @@ contract OffchainResolver is return (address(_verifier), _gateways); } - /// @notice Set `signer` as an trusted signer. - function setSigner(address signer, bool enabled) external onlyOwner { - require(isOffchainSigner[signer] != enabled); - isOffchainSigner[signer] = enabled; - emit SignerChanged(signer, enabled); + /// @notice Set the gateways. + function setGateways(string[] memory gateways) external onlyOwner { + _gateways = gateways; + emit GatewaysChanged(gateways); } - /// @notice Set the gateways. - function setGateways(string[] memory gateways_) external onlyOwner { - _gateways = gateways_; - emit GatewaysChanged(gateways_); + /// @notice Set the verifier. + function setVerifier(IOffchainVerifier verifier) external onlyOwner { + _verifier = verifier; + emit VerifierChanged(hex"00", address(verifier)); } /// @inheritdoc IExtendedResolver diff --git a/src/OffchainVerifier.sol b/src/OffchainVerifier.sol index ba6400b..cfef790 100755 --- a/src/OffchainVerifier.sol +++ b/src/OffchainVerifier.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.13; +import {Ownable} from "@oz/access/Ownable.sol"; import {ERC165} from "@oz/utils/introspection/ERC165.sol"; import {ECDSA} from "@oz/utils/cryptography/ECDSA.sol"; import { @@ -9,6 +10,39 @@ import { } from "./IOffchainVerifier.sol"; contract OffchainVerifier is ERC165, IOffchainVerifier { + event SignerChanged(address target, address signer, bool enabled); + + mapping(address target => mapping(address signer => bool)) _isSigner; + + /// @notice Set signer for target. + function setOffchainSigner( + address target, + address signer, + bool enabled + ) external { + if (target == address(0) || Ownable(target).owner() != msg.sender) { + revert Ownable.OwnableUnauthorizedAccount(target); + } + _isSigner[target][signer] = enabled; + emit SignerChanged(target, signer, enabled); + } + + function isOffchainSigner( + address target, + address signer + ) public view returns (bool) { + if (_isSigner[target][signer]) { + return true; + } + try + IOffchainVerifierSigner(target).isOffchainSigner(signer) + returns (bool enabled) { + return enabled; + } catch { + return false; + } + } + /// @inheritdoc ERC165 function supportsInterface( bytes4 interfaceId @@ -42,7 +76,7 @@ contract OffchainVerifier is ERC165, IOffchainVerifier { ) ); address signed = ECDSA.recover(hash, sig); - if (!IOffchainVerifierSigner(msg.sender).isOffchainSigner(signed)) { + if (!isOffchainSigner(msg.sender, signed)) { revert CCIPReadUntrusted(signed); } return answer; diff --git a/test/e2e.test.ts b/test/e2e.test.ts index 08265a8..c87a600 100755 --- a/test/e2e.test.ts +++ b/test/e2e.test.ts @@ -1,12 +1,5 @@ -import { - afterAll, - afterEach, - beforeAll, - describe, - expect, - test, -} from "bun:test"; -import { Foundry, type DeployedContract } from "@adraffy/blocksmith"; +import { afterAll, afterEach, describe, expect, test } from "bun:test"; +import { Foundry } from "@adraffy/blocksmith"; import { RESOLVE_ABI } from "@namestone/ezccip"; import { serve } from "@namestone/ezccip/serve"; import { dnsEncode, namehash, id as labelhash, ZeroHash } from "ethers"; @@ -29,7 +22,7 @@ describe("e2e", async () => { }); const OR = await F.deploy({ file: "OffchainResolver", - args: [admin, OffchainVerifier, [], []], + args: [admin], }); // setup offchain server const S = await serve( @@ -58,14 +51,20 @@ describe("e2e", async () => { }, { protocol: "ens" } ); + expect( + F.getEventResults( + await F.confirm(OR.setVerifier(OffchainVerifier)), + "VerifierChanged" + ) + ).toEqual<[[string, string]]>([["0x00", OffchainVerifier.target]]); // ezccip generates a random key if not specified // add this key as a trusted signer expect( F.getEventResults( - await F.confirm(OR.setSigner(S.signer, true)), + await F.confirm(OffchainVerifier.setOffchainSigner(OR, S.signer, true)), "SignerChanged" ) - ).toEqual<[[string, boolean]]>([[S.signer, true]]); + ).toEqual<[[string, string, boolean]]>([[OR.target, S.signer, true]]); // to support recursive ccip-read, we ignore the ccip sender, // and instead sign relative to the contract we're supporting. // note: ezccip automatically interprets the first address @@ -109,11 +108,11 @@ describe("e2e", async () => { ); test("toggle signer", async () => { - expect(OR.isOffchainSigner(ETH)).resolves.toBeFalse(); - await F.confirm(OR.setSigner(ETH, true)); - expect(OR.isOffchainSigner(ETH)).resolves.toBeTrue(); - await F.confirm(OR.setSigner(ETH, false)); - expect(OR.isOffchainSigner(ETH)).resolves.toBeFalse(); + expect(OffchainVerifier.isOffchainSigner(OR, ETH)).resolves.toBeFalse(); + await F.confirm(OffchainVerifier.setOffchainSigner(OR, ETH, true)); + expect(OffchainVerifier.isOffchainSigner(OR, ETH)).resolves.toBeTrue(); + await F.confirm(OffchainVerifier.setOffchainSigner(OR, ETH, false)); + expect(OffchainVerifier.isOffchainSigner(OR, ETH)).resolves.toBeFalse(); }); test("UnreachableName", async () => { @@ -290,16 +289,15 @@ describe("e2e", async () => { test("UR: multicall() w/o Multicall feature", async () => { // deploy a modified OffchainResolver disabled features - const HR = await F.deploy({ - sol: `import "@src/OffchainResolver.sol"; - contract HR is OffchainResolver { - constructor(address[] memory ss, IOffchainVerifier v) OffchainResolver(msg.sender, v, ss, new string[](0)) {} - function supportsFeature(bytes4) external pure override returns (bool) { - return false; - } - }`, - args: [[S.signer], OffchainVerifier], - }); + const HR = await F.deploy(`import "@src/OffchainResolver.sol"; + contract HR is OffchainResolver(msg.sender) { + function supportsFeature(bytes4) external pure override returns (bool) { + return false; + } + } + `); + await F.confirm(HR.setVerifier(OffchainVerifier)); + await F.confirm(OffchainVerifier.setOffchainSigner(HR, S.signer, true)); await F.confirm(HR.setGateways([`${S.endpoint}/${HR.target}`])); await F.confirm(ENS.setResolver(namehash("raffy.eth"), HR)); // replace resolver in registry // setup local batch gateway to facilitate multicall