diff --git a/contracts/resolvers/ParentAvatarResolver.sol b/contracts/resolvers/ParentAvatarResolver.sol new file mode 100644 index 00000000..a67f7ebd --- /dev/null +++ b/contracts/resolvers/ParentAvatarResolver.sol @@ -0,0 +1,63 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.17 <0.9.0; + +import {ENS} from "../registry/ENS.sol"; +import {INameWrapper} from "../wrapper/INameWrapper.sol"; +import {PublicResolver} from "./PublicResolver.sol"; + +error AvatarCannotBeSetByOwner(); + +/** + * + * This resolver allows the owner of a node to set the avatar of a child node. + * + */ +contract ParentAvatarResolver is PublicResolver { + constructor( + ENS _ens, + INameWrapper wrapperAddress, + address _trustedETHController, + address _trustedReverseRegistrar + ) + PublicResolver( + _ens, + wrapperAddress, + _trustedETHController, + _trustedReverseRegistrar + ) + {} + + function setText( + bytes32 node, + string calldata key, + string calldata value + ) external override authorised(node) { + // check if key is avatar + if ( + keccak256(abi.encodePacked(key)) == + keccak256(abi.encodePacked("avatar")) + ) { + revert AvatarCannotBeSetByOwner(); + } + + _setText(node, key, value); + } + + function setAvatar( + bytes32 parentNode, + bytes32 labelhash, + string calldata value + ) external authorised(parentNode) { + bytes32 node = keccak256(abi.encodePacked(parentNode, labelhash)); + _setText(node, "avatar", value); + } + + function _setText( + bytes32 node, + string memory key, + string calldata value + ) internal { + versionable_texts[recordVersions[node]][node][key] = value; + emit TextChanged(node, key, key, value); + } +} diff --git a/contracts/resolvers/ResolverBase.sol b/contracts/resolvers/ResolverBase.sol index 3eb8ba73..a9b2db9c 100644 --- a/contracts/resolvers/ResolverBase.sol +++ b/contracts/resolvers/ResolverBase.sol @@ -4,13 +4,17 @@ pragma solidity >=0.8.4; import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import "./profiles/IVersionableResolver.sol"; +error NotAuthorised(); + abstract contract ResolverBase is ERC165, IVersionableResolver { mapping(bytes32 => uint64) public recordVersions; function isAuthorised(bytes32 node) internal view virtual returns (bool); modifier authorised(bytes32 node) { - require(isAuthorised(node)); + if (!isAuthorised(node)) { + revert NotAuthorised(); + } _; } diff --git a/test/resolvers/TestParentAvatarResolver.js b/test/resolvers/TestParentAvatarResolver.js new file mode 100644 index 00000000..d3bdb877 --- /dev/null +++ b/test/resolvers/TestParentAvatarResolver.js @@ -0,0 +1,86 @@ +const ENS = artifacts.require('./registry/ENSRegistry.sol') +const NameWrapper = artifacts.require('DummyNameWrapper.sol') +const { deploy } = require('../test-utils/contracts') +const { labelhash } = require('../test-utils/ens') +const { EMPTY_BYTES32: ROOT_NODE } = require('../test-utils/constants') + +const { expect } = require('chai') +const namehash = require('eth-ens-namehash') + +contract('Parent Avatar Resolver', function (accounts) { + let node + let ens, resolver, resolver2, nameWrapper + let account + let signers + + beforeEach(async () => { + signers = await ethers.getSigners() + account = await signers[0].getAddress() + node = namehash.hash('eth') + ens = await ENS.new() + nameWrapper = await NameWrapper.new() + + //setup reverse registrar + + const ReverseRegistrar = await deploy('ReverseRegistrar', ens.address) + + await ens.setSubnodeOwner(ROOT_NODE, labelhash('reverse'), account) + await ens.setSubnodeOwner( + namehash.hash('reverse'), + labelhash('addr'), + ReverseRegistrar.address, + ) + + resolver = await deploy( + 'ParentAvatarResolver', + ens.address, + nameWrapper.address, + accounts[9], // trusted contract + ReverseRegistrar.address, //ReverseRegistrar.address, + ) + + resolver2 = resolver.connect(signers[1]) + + await ReverseRegistrar.setDefaultResolver(resolver.address) + + await ens.setSubnodeOwner('0x0', labelhash('eth'), accounts[0], { + from: accounts[0], + }) + }) + + describe('setText()', () => { + it('should set text', async () => { + await resolver.setText(node, 'url', 'https://example.com', { + from: accounts[0], + }) + const result = await resolver.text(node, 'url') + expect(result).to.equal('https://example.com') + }) + + it('should not be able to set avatar', async () => { + await expect( + resolver.setText(node, 'avatar', 'https://example.com', { + from: accounts[0], + }), + ).to.be.revertedWith('AvatarCannotBeSetByOwner()') + }) + }) + + describe('setAvatar()', () => { + it('should be able to set avatar as the parentOwner', async () => { + resolver.setAvatar(ROOT_NODE, labelhash('eth'), 'https://example.com', { + from: accounts[0], + }) + + const result = await resolver.text(node, 'avatar') + expect(result).to.equal('https://example.com') + }) + + it('should not able to set avatar if not the parent Owner', async () => { + ens.setSubnodeOwner('0x0', labelhash('eth'), accounts[1]) + await expect( + resolver2.setAvatar(ROOT_NODE, labelhash('eth'), 'https://example.com'), + ).to.be.revertedWith('NotAuthorised()') + }) + }) +})