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 17 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
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
126 changes: 126 additions & 0 deletions contracts/resolvers/DelegatableResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
pragma solidity >=0.8.4;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
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";

/**
* 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
Ownable,
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);

constructor(address owner) {
operators[bytes32(0)][owner] = true;
makoto marked this conversation as resolved.
Show resolved Hide resolved
transferOwnership(owner);
}

//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 getAuthorizedNode(
makoto marked this conversation as resolved.
Show resolved Hide resolved
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) = getAuthorizedNode(
name,
offset + len + 1,
operator
);
node = keccak256(abi.encodePacked(node, label));
}
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) = getAuthorizedNode(
name,
0,
msg.sender
);
if (!authorized) {
revert NotAuthorized(node);
}
operators[node][operator] = approved;
emit Approval(node, operator, name, approved);
}

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);
}
}
16 changes: 16 additions & 0 deletions contracts/resolvers/IDelegatableResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;

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

function getAuthorizedNode(
bytes memory name,
uint256 offset,
address operator
) external returns (bytes32 node, bool authorized);
}
182 changes: 182 additions & 0 deletions test/resolvers/TestDelegatableResolver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
const DelegatableResolver = artifacts.require('DelegatableResolver.sol')
const { encodeName, namehash } = require('../test-utils/ens')
const { exceptions } = require('../test-utils')
const { expect } = require('chai')

contract('DelegatableResolver', function (accounts) {
let node
let encodedname
let resolver
let signers
let deployer
let owner
let operator
let operator2

beforeEach(async () => {
signers = await ethers.getSigners()
deployer = await signers[0].getAddress()
owner = await signers[1].getAddress()
operator = await signers[2].getAddress()
operator2 = await signers[3].getAddress()
node = namehash('eth')
encodedname = encodeName('eth')
resolver = await DelegatableResolver.new(owner)
})

describe('supportsInterface function', async () => {
it('supports known interfaces', async () => {
assert.equal(await resolver.supportsInterface('0x3b3b57de'), true) // IAddrResolver
assert.equal(await resolver.supportsInterface('0xf1cb7e06'), true) // IAddressResolver
assert.equal(await resolver.supportsInterface('0x691f3431'), true) // INameResolver
assert.equal(await resolver.supportsInterface('0x2203ab56'), true) // IABIResolver
assert.equal(await resolver.supportsInterface('0xc8690233'), true) // IPubkeyResolver
assert.equal(await resolver.supportsInterface('0x59d1d43c'), true) // ITextResolver
assert.equal(await resolver.supportsInterface('0xbc1c58d1'), true) // IContentHashResolver
assert.equal(await resolver.supportsInterface('0xa8fa5682'), true) // IDNSRecordResolver
assert.equal(await resolver.supportsInterface('0x5c98042b'), true) // IDNSZoneResolver
assert.equal(await resolver.supportsInterface('0x01ffc9a7'), true) // IInterfaceResolver
assert.equal(await resolver.supportsInterface('0x4fbf0433'), true) // IMulticallable
assert.equal(await resolver.supportsInterface('0xdd48591c'), true) // IDelegatable
})

it('does not support a random interface', async () => {
assert.equal(await resolver.supportsInterface('0x3b3b57df'), false)
})
})

describe('addr', async () => {
it('permits setting address by owner', async () => {
await resolver.methods['setAddr(bytes32,address)'](node, operator, {
from: owner,
})
assert.equal(await resolver.methods['addr(bytes32)'](node), operator)
})

it('forbids setting new address by non-owners', async () => {
await exceptions.expectFailure(
resolver.methods['setAddr(bytes32,address)'](node, operator, {
from: operator,
}),
)
})

it('forbids approving wrong node', async () => {
encodedname = encodeName('a.b.c.eth')
const wrongnode = namehash('d.b.c.eth')
await resolver.approve(encodedname, operator, true, { from: owner })
await exceptions.expectFailure(
resolver.methods['setAddr(bytes32,address)'](wrongnode, operator, {
from: operator,
}),
)
})
})

describe('authorisations', async () => {
makoto marked this conversation as resolved.
Show resolved Hide resolved
it('owner is the owner', async () => {
assert.equal(await resolver.owner(), owner)
})

it('owner is ahtorised to update any names', async () => {
assert.equal(
(await resolver.getAuthorizedNode(encodeName('a.b.c'), 0, owner))
.authorized,
true,
)
assert.equal(
(await resolver.getAuthorizedNode(encodeName('x.y.z'), 0, owner))
.authorized,
true,
)
})

it('approves multiple users', async () => {
await resolver.approve(encodedname, operator, true, { from: owner })
await resolver.approve(encodedname, operator2, true, { from: owner })
const result = await resolver.getAuthorizedNode(encodedname, 0, operator)
assert.equal(result.node, node)
assert.equal(result.authorized, true)
assert.equal(
(await resolver.getAuthorizedNode(encodedname, 0, operator2))
.authorized,
true,
)
})

it('approves subnames', async () => {
const subname = 'a.b.c.eth'
await resolver.approve(encodeName(subname), operator, true, {
from: owner,
})
await resolver.methods['setAddr(bytes32,address)'](
namehash(subname),
operator,
{
from: operator,
},
)
})

it('only approves the subname and not its parent', async () => {
const subname = '1234.123'
const parentname = 'b.c.eth'
await resolver.approve(encodeName(subname), operator, true, {
from: owner,
})
const result = await resolver.getAuthorizedNode(
encodeName(subname),
0,
operator,
)
assert.equal(result.node, namehash(subname))
assert.equal(result.authorized, true)
const result2 = await resolver.getAuthorizedNode(
encodeName(parentname),
0,
operator,
)
assert.equal(result2.node, namehash(parentname))
assert.equal(result2.authorized, false)
})

it('approves users to make changes', async () => {
await resolver.approve(encodedname, operator, true, { from: owner })
await resolver.methods['setAddr(bytes32,address)'](node, operator, {
from: operator,
})
assert.equal(await resolver.addr(node), operator)
})

it('approves to be revoked', async () => {
await resolver.approve(encodedname, operator, true, { from: owner })
resolver.methods['setAddr(bytes32,address)'](node, operator2, {
from: operator,
}),
await resolver.approve(encodedname, operator, false, { from: owner })
await exceptions.expectFailure(
resolver.methods['setAddr(bytes32,address)'](node, operator2, {
from: operator,
}),
)
})

it('does not allow non owner to approve', async () => {
await expect(
resolver.approve(encodedname, operator, true, { from: operator }),
).to.be.revertedWith('NotAuthorized')
})

it('emits an Approval log', async () => {
var tx = await resolver.approve(encodedname, operator, true, {
from: owner,
})
assert.equal(tx.logs.length, 1)
assert.equal(tx.logs[0].event, 'Approval')
assert.equal(tx.logs[0].args.node, node)
assert.equal(tx.logs[0].args.operator, operator)
assert.equal(tx.logs[0].args.name, encodedname)
assert.equal(tx.logs[0].args.approved, true)
})
})
})