Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions src/IOffchainVerifier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.13;

interface IOffchainVerifierSigner {
function isOffchainSigner(address) external view returns (bool);
}

interface IOffchainVerifier {
error CCIPReadExpired(uint64 expiry);
error CCIPReadUntrusted(address signed);

/// @notice Verify `response` was signed by an authorized account.
function verifyResponse(
bytes calldata request,
bytes calldata response
) external view returns (bytes memory);
}
86 changes: 21 additions & 65 deletions src/OffchainResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,37 @@ 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";
import {IOffchainVerifier} from "./IOffchainVerifier.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.
mapping(address signer => bool enabled) public isSigner;

constructor(
address owner,
address[] memory signers,
string[] memory gateways_
) Ownable(owner) {
for (uint256 i; i < signers.length; ++i) {
address signer = signers[i];
isSigner[signer] = true;
emit SignerChanged(signer, true);
}
_gateways = gateways_;
emit GatewaysChanged(gateways_);
}
constructor(address owner) Ownable(owner) {}

/// @inheritdoc ERC165
function supportsInterface(
bytes4 interfaceId
) 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);
}

Expand All @@ -60,22 +44,23 @@ contract OffchainResolver is
return featureId == ResolverFeatures.RESOLVE_MULTICALL;
}

/// @notice Set `signer` as an trusted signer.
function setSigner(address signer, bool enabled) external onlyOwner {
require(isSigner[signer] != enabled);
isSigner[signer] = enabled;
emit SignerChanged(signer, enabled);
/// @inheritdoc IVerifiableResolver
function verifierMetadata(
bytes calldata /*name*/
) external view returns (address verifier, string[] memory gateways) {
return (address(_verifier), _gateways);
}

/// @notice Set the gateways.
function setGateways(string[] memory gateways_) external onlyOwner {
_gateways = gateways_;
emit GatewaysChanged(gateways_);
function setGateways(string[] memory gateways) external onlyOwner {
_gateways = gateways;
emit GatewaysChanged(gateways);
}

/// @inheritdoc IGatewayProvider
function gateways() external view returns (string[] memory) {
return _gateways;
/// @notice Set the verifier.
function setVerifier(IOffchainVerifier verifier) external onlyOwner {
_verifier = verifier;
emit VerifierChanged(hex"00", address(verifier));
}

/// @inheritdoc IExtendedResolver
Expand All @@ -97,35 +82,6 @@ contract OffchainResolver is
bytes calldata response,
bytes calldata request
) external view returns (bytes memory) {
return _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;
return _verifier.verifyResponse(request, response);
}
}
84 changes: 84 additions & 0 deletions src/OffchainVerifier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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 {
IOffchainVerifier,
IOffchainVerifierSigner
} 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
) public view override returns (bool) {
return
interfaceId == type(IOffchainVerifier).interfaceId ||
super.supportsInterface(interfaceId);
}

/// @inheritdoc IOffchainVerifier
function verifyResponse(
bytes calldata request,
bytes calldata response
) external 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);
}
/// forge-lint: disable-next-item(asm-keccak256)
// standard "ens" offchain signing protocol
bytes32 hash = keccak256(
abi.encodePacked(
bytes2(0x1900),
msg.sender,
expiry,
keccak256(request), // original calldata, eg. msg.data
keccak256(answer) // response from server
)
);
address signed = ECDSA.recover(hash, sig);
if (!isOffchainSigner(msg.sender, signed)) {
revert CCIPReadUntrusted(signed);
}
return answer;
}
}
Loading