Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delegatable resolver #288

Open
wants to merge 21 commits into
base: staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@ yarn pub
5. Create a "Release Candidate" [release](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases) on GitHub. This will be of the form `v1.2.3-RC0`. This tagged commit is now subject to our bug bounty.
6. Have the tagged commit audited if necessary
7. If changes are required, make the changes and then once ready for review create another GitHub release with an incremented RC value `v1.2.3-RC0` -> `v.1.2.3-RC1`. Repeat as necessary.
8. Deploy to testnet. Open a pull request to merge the deploy artifacts into
the `feature` branch. Get someone to review and approve the deployment and then merge. You now MUST merge this branch into `staging` branch.
8. Deploy to testnet. Open a pull request to merge the deploy artifacts into
the `feature` branch. Get someone to review and approve the deployment and then merge. You now MUST merge this branch into `staging` branch.
9. Create GitHub release of the form `v1.2.3-testnet` from the commit that has the new deployment artifacts.
10. If any further changes are needed, you can either make them on the existing feature branch that is in sync or create a new branch, and follow steps 1 -> 9. Repeat as necessary.
11. Make Deployment to mainnet from `staging`. Commit build artifacts. You now MUST merge this branch into `main`.
Expand Down
134 changes: 134 additions & 0 deletions contracts/resolvers/DelegatableResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
pragma solidity >=0.8.4;
import "./profiles/ABIResolver.sol";
import "./profiles/AddrResolver.sol";
import "./profiles/ContentHashResolver.sol";
import "./profiles/DNSResolver.sol";
import "./profiles/InterfaceResolver.sol";
import "./profiles/NameResolver.sol";
import "./profiles/PubkeyResolver.sol";
import "./profiles/TextResolver.sol";
import "./profiles/ExtendedResolver.sol";
import "./Multicallable.sol";
import "./IDelegatableResolver.sol";
import {Clone} from "clones-with-immutable-args/src/Clone.sol";

/**
* A delegated resolver that allows the resolver owner to add an operator to update records of a node on behalf of the owner.
* address.
*/
contract DelegatableResolver is
Clone,
Multicallable,
ABIResolver,
AddrResolver,
ContentHashResolver,
DNSResolver,
InterfaceResolver,
NameResolver,
PubkeyResolver,
TextResolver,
ExtendedResolver
{
using BytesUtils for bytes;

// Logged when an operator is added or removed.
event Approval(
bytes32 indexed node,
address indexed operator,
bytes name,
bool approved
);

error NotAuthorized(bytes32 node);

//node => (delegate => isAuthorised)
mapping(bytes32 => mapping(address => bool)) operators;

/*
* Check to see if the operator has been approved by the owner for the node.
* @param name The ENS node to query
* @param offset The offset of the label to query recursively. Start from the 0 position and kepp adding the length of each label as it traverse. The function exits when len is 0.
* @param operator The address of the operator to query
* @return node The node of the name passed as an argument
* @return authorized The boolean state of whether the operator is approved to update record of the name
*/
function getAuthorisedNode(
bytes memory name,
uint256 offset,
address operator
) public view returns (bytes32 node, bool authorized) {
uint256 len = name.readUint8(offset);
node = bytes32(0);
if (len > 0) {
makoto marked this conversation as resolved.
Show resolved Hide resolved
bytes32 label = name.keccak(offset + 1, len);
(node, authorized) = getAuthorisedNode(
name,
offset + len + 1,
operator
);
node = keccak256(abi.encodePacked(node, label));
} else {
return (
node,
authorized || operators[node][operator] || owner() == operator
);
}
return (node, authorized || operators[node][operator]);
}

/**
* @dev Approve an operator to be able to updated records on a node.
*/
function approve(
bytes memory name,
address operator,
bool approved
) external {
(bytes32 node, bool authorized) = getAuthorisedNode(
name,
0,
msg.sender
);
if (!authorized) {
revert NotAuthorized(node);
}
operators[node][operator] = approved;
emit Approval(node, operator, name, approved);
}

/*
* Returns the owner address passed set by the Factory
* @return address The owner address
*/
function owner() public view returns (address) {
return _getArgAddress(0);
}

function isAuthorised(bytes32 node) internal view override returns (bool) {
return msg.sender == owner() || operators[node][msg.sender];
}

function supportsInterface(
bytes4 interfaceID
)
public
view
virtual
override(
Multicallable,
ABIResolver,
AddrResolver,
ContentHashResolver,
DNSResolver,
InterfaceResolver,
NameResolver,
PubkeyResolver,
TextResolver
)
returns (bool)
{
return
interfaceID == type(IDelegatableResolver).interfaceId ||
super.supportsInterface(interfaceID);
}
}
43 changes: 43 additions & 0 deletions contracts/resolvers/DelegatableResolverFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "./DelegatableResolver.sol";
import {ClonesWithImmutableArgs} from "clones-with-immutable-args/src/ClonesWithImmutableArgs.sol";

/**
* A resolver factory that creates a dedicated resolver for each user
*/

contract DelegatableResolverFactory {
using ClonesWithImmutableArgs for address;

DelegatableResolver public implementation;
event NewDelegatableResolver(address resolver, address owner);

constructor(DelegatableResolver _implementation) {
implementation = _implementation;
}

/*
* Create the unique address unique to the owner
* @param address The address of the resolver owner
* @return address The address of the newly created Resolver
*/
function create(
address owner
) external returns (DelegatableResolver clone) {
bytes memory data = abi.encodePacked(owner);
clone = DelegatableResolver(address(implementation).clone2(data));
emit NewDelegatableResolver(address(clone), owner);
}

/*
* Returns the unique address unique to the owner
* @param address The address of the resolver owner
* @return address The address of the newly created Resolver
*/
function predictAddress(address owner) external returns (address clone) {
bytes memory data = abi.encodePacked(owner);
clone = address(implementation).predictAddress(data);
}
}
18 changes: 18 additions & 0 deletions contracts/resolvers/IDelegatableResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;

interface IDelegatableResolver {
function approve(
bytes memory name,
address operator,
bool approved
) external;

function getAuthorisedNode(
bytes memory name,
uint256 offset,
address operator
) external returns (bytes32 node, bool authorized);

function owner() external view returns (address);
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@ensdomains/buffer": "^0.1.1",
"@ensdomains/solsha1": "0.0.3",
"@openzeppelin/contracts": "^4.1.0",
"clones-with-immutable-args": "Arachnid/clones-with-immutable-args#feature/create2",
"dns-packet": "^5.3.0"
},
"directories": {
Expand Down