Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
stevedylandev committed Jan 25, 2025
1 parent 7249900 commit e39e7fe
Show file tree
Hide file tree
Showing 8 changed files with 317 additions and 57 deletions.
6 changes: 6 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
ens-contracts/=lib/ens-contracts/contracts/
erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/
forge-std/=lib/forge-std/src/
halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/
openzeppelin-contracts/=lib/openzeppelin-contracts/
19 changes: 0 additions & 19 deletions script/Counter.s.sol

This file was deleted.

27 changes: 27 additions & 0 deletions script/IPCMResolver.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";
import {HybridResolver} from "../src/IPCMResolver.sol";
import {ENS} from "../lib/ens-contracts/contracts/registry/ENS.sol";
import {INameWrapper} from "../lib/ens-contracts/contracts/wrapper/INameWrapper.sol";

contract DeployResolver is Script {
HybridResolver public resolver;

function setUp() public {}

function run() public {
vm.startBroadcast();

ENS ens = ENS(vm.envAddress("ENS_REGISTRY"));
INameWrapper nameWrapper = INameWrapper(vm.envAddress("NAME_WRAPPER"));
string memory url = vm.envString("GATEWAY_URL");
address signer = vm.envAddress("SIGNER");
address owner = vm.envAddress("OWNER");

resolver = new HybridResolver(ens, nameWrapper, url, signer, owner);

vm.stopBroadcast();
}
}
14 changes: 0 additions & 14 deletions src/Counter.sol

This file was deleted.

171 changes: 171 additions & 0 deletions src/IPCMResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "../lib/ens-contracts/contracts/registry/ENS.sol";
import "../lib/ens-contracts/contracts/wrapper/INameWrapper.sol";
import "../lib/ens-contracts/contracts/resolvers/profiles/IExtendedResolver.sol";
import "../lib/ens-contracts/contracts/resolvers/profiles/ABIResolver.sol";
import "../lib/ens-contracts/contracts/resolvers/profiles/AddrResolver.sol";
import "../lib/ens-contracts/contracts/resolvers/profiles/ContentHashResolver.sol";
import "../lib/ens-contracts/contracts/resolvers/profiles/NameResolver.sol";
import "../lib/ens-contracts/contracts/resolvers/profiles/TextResolver.sol";
import "../lib/ens-contracts/contracts/resolvers/Multicallable.sol";
import "../lib/ens-contracts/contracts/resolvers/profiles/InterfaceResolver.sol";
import "../lib/openzeppelin-contracts/contracts/access/Ownable.sol";

import "./SignatureVerifier.sol";

interface IResolverService {
function resolve(
bytes calldata name,
bytes calldata data
)
external
view
returns (bytes memory result, uint64 expires, bytes memory sig);
}

/**
* A hybrid onchain/offchain ENS resolver contract for flexible record management.
*/
contract HybridResolver is
Multicallable,
ABIResolver,
AddrResolver,
ContentHashResolver,
InterfaceResolver,
NameResolver,
TextResolver,
IExtendedResolver,
Ownable
{
ENS immutable ens;
INameWrapper immutable nameWrapper;
string public url;
address public signer;

error OffchainLookup(
address sender,
string[] urls,
bytes callData,
bytes4 callbackFunction,
bytes extraData
);

constructor(
ENS _ens,
INameWrapper _nameWrapper,
string memory _url,
address _signer,
address _owner
) Ownable(_owner) {
ens = _ens;
nameWrapper = _nameWrapper;
url = _url;
signer = _signer;
}

function resolve(
bytes calldata name,
bytes memory data
) external view virtual returns (bytes memory) {
(bool success, bytes memory result) = address(this).staticcall(data);
bytes32 hashedResult = keccak256(result);

// keccak256(0x0000000000000000000000000000000000000000000000000000000000000000)
// covers empty addr(node), name(node), contenthash(node)
bytes32 emptySingleArg = 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563;

// keccak256(0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000)
// covers addr(node, coinType), text(node, key), ABI(node, contentTypes)
bytes32 emptyDoubleArg = 0x569e75fc77c1a856f6daaf9e69d8a9566ca34aa47f9133711ce065a571af0cfd;

if (
success &&
(hashedResult != emptySingleArg) &&
(hashedResult != emptyDoubleArg)
) {
// If we have an onchain result, return it
return result;
} else {
// Otherwise, fallback to offchain lookup
return resolveOffchain(name, data);
}
}

function resolveOffchain(
bytes calldata name,
bytes memory data
) internal view virtual returns (bytes memory) {
bytes memory callData = abi.encodeWithSelector(
IResolverService.resolve.selector,
name,
data
);
string[] memory urls = new string[](1);
urls[0] = url;
revert OffchainLookup(
address(this),
urls,
callData,
HybridResolver.resolveWithProof.selector,
abi.encode(callData, address(this))
);
}

function resolveWithProof(
bytes calldata response,
bytes calldata extraData
) external view returns (bytes memory) {
(address _signer, bytes memory result) = SignatureVerifier.verify(
extraData,
response
);
require(_signer == signer, "SignatureVerifier: Invalid sigature");
return result;
}

function supportsInterface(
bytes4 interfaceID
)
public
view
override(
Multicallable,
ABIResolver,
AddrResolver,
ContentHashResolver,
InterfaceResolver,
NameResolver,
TextResolver
)
returns (bool)
{
return
interfaceID == type(IExtendedResolver).interfaceId ||
super.supportsInterface(interfaceID);
}

function isAuthorised(bytes32 node) internal view override returns (bool) {
address owner = ens.owner(node);

if (
owner == address(nameWrapper)
? !nameWrapper.canModifyName(node, msg.sender)
: (owner != msg.sender &&
!ens.isApprovedForAll(owner, msg.sender))
) {
return false;
}

return true;
}

function setUrl(string memory _url) external onlyOwner {
url = _url;
}

function setSigner(address _signer) external onlyOwner {
signer = _signer;
}
}
61 changes: 61 additions & 0 deletions src/SignatureVerifier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "../lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";

library SignatureVerifier {
/**
* @dev Generates a hash for signing/verifying.
* @param target: The address the signature is for.
* @param request: The original request that was sent.
* @param result: The `result` field of the response (not including the signature part).
*/
function makeSignatureHash(
address target,
uint64 expires,
bytes memory request,
bytes memory result
) internal pure returns (bytes32) {
return
keccak256(
abi.encodePacked(
hex"1900",
target,
expires,
keccak256(request),
keccak256(result)
)
);
}

/**
* @dev Verifies a signed message returned from a callback.
* @param request: The original request that was sent.
* @param response: An ABI encoded tuple of `(bytes result, uint64 expires, bytes sig)`, where `result` is the data to return
* to the caller, and `sig` is the (r,s,v) encoded message signature.
* @return signer: The address that signed this message.
* @return result: The `result` decoded from `response`.
*/
function verify(
bytes calldata request,
bytes calldata response
) internal view returns (address, bytes memory) {
(bytes memory result, uint64 expires, bytes memory sig) = abi.decode(
response,
(bytes, uint64, bytes)
);
(bytes memory extraData, address sender) = abi.decode(
request,
(bytes, address)
);
address signer = ECDSA.recover(
makeSignatureHash(sender, expires, extraData, result),
sig
);
require(
expires >= block.timestamp,
"SignatureVerifier: Signature expired"
);
return (signer, result);
}
}
24 changes: 0 additions & 24 deletions test/Counter.t.sol

This file was deleted.

52 changes: 52 additions & 0 deletions test/IPCMResolver.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console} from "forge-std/Test.sol";
import {HybridResolver} from "../src/IPCMResolver.sol";
import "../lib/ens-contracts/contracts/registry/ENS.sol";
import "../lib/ens-contracts/contracts/wrapper/INameWrapper.sol";
import "../lib/ens-contracts/contracts/registry/ENSRegistry.sol";

contract IPCMResolverTest is Test {
HybridResolver public resolver;
ENS public ens;
INameWrapper public nameWrapper;

address signer = address(0x123);
address owner = address(0x456);
string url = "https://example.com";

function setUp() public {
ens = new ENSRegistry(); // Use concrete implementation instead of interface
nameWrapper = INameWrapper(address(0x789)); // Mock wrapper
resolver = new HybridResolver(ens, nameWrapper, url, signer, owner);
}

function testSetUrl() public {
string memory newUrl = "https://newexample.com";
vm.prank(owner);
resolver.setUrl(newUrl);
assertEq(resolver.url(), newUrl);
}

function testSetSigner() public {
address newSigner = address(0xabc);
vm.prank(owner);
resolver.setSigner(newSigner);
assertEq(resolver.signer(), newSigner);
}

// function testOnlyOwnerCanSetUrl() public {
// string memory newUrl = "https://newexample.com";
// vm.prank(address(0xdef));
// vm.expectRevert("Ownable: caller is not the owner");
// resolver.setUrl(newUrl);
// }

// function testOnlyOwnerCanSetSigner() public {
// address newSigner = address(0xabc);
// vm.prank(address(0xdef));
// vm.expectRevert("Ownable: caller is not the owner");
// resolver.setSigner(newSigner);
// }
}

0 comments on commit e39e7fe

Please sign in to comment.