From 1f294951af29e593ff7f8b648c2a263354332595 Mon Sep 17 00:00:00 2001 From: nxt3d Date: Sat, 20 Jan 2024 17:49:41 -0500 Subject: [PATCH 01/15] add token approvals and add ROOT_NODE --- contracts/l2/IController.sol | 2 +- contracts/l2/L2Registry.sol | 60 +++++++++++++++++++++---- contracts/l2/RootController.sol | 13 +++++- contracts/l2/SimpleController.sol | 30 +++++++------ test/l2/TestL2Registry.js | 75 ++++++++++++++++++++++++++++--- 5 files changed, 149 insertions(+), 31 deletions(-) diff --git a/contracts/l2/IController.sol b/contracts/l2/IController.sol index 3eb36ffc4..aac30ef06 100644 --- a/contracts/l2/IController.sol +++ b/contracts/l2/IController.sol @@ -13,7 +13,7 @@ interface IController { uint256 id, uint256 value, bytes calldata data, - bool operatorApproved + bool isApproved ) external returns (bytes memory); function balanceOf( diff --git a/contracts/l2/L2Registry.sol b/contracts/l2/L2Registry.sol index 611d93359..9204a75b7 100644 --- a/contracts/l2/L2Registry.sol +++ b/contracts/l2/L2Registry.sol @@ -10,6 +10,8 @@ import "./IController.sol"; contract L2Registry is IERC1155 { mapping(uint256 => bytes) public tokens; mapping(address => mapping(address => bool)) approvals; + mapping(address => uint256) tokenApprovalsNonce; + mapping(address => mapping(uint256 => mapping(uint256 => mapping(address => bool)))) tokenApprovals; error TokenDoesNotExist(uint256 id); @@ -53,6 +55,25 @@ contract L2Registry is IERC1155 { emit ApprovalForAll(msg.sender, operator, approved); } + // set approval for id + function setApprovalForId( + address delegate, + uint256 id, + bool approved + ) external { + // get the owner of the token + address _owner = _getController(tokens[id]).ownerOf(tokens[id]); + // make sure the caller is the owner or an approved operator. + require( + msg.sender == _owner || isApprovedForAll(_owner, msg.sender), + "L2Registry: caller is not owner or approved operator" + ); + + tokenApprovals[_owner][tokenApprovalsNonce[_owner]][id][ + delegate + ] = approved; + } + /************************* * Public view functions * *************************/ @@ -87,18 +108,37 @@ contract L2Registry is IERC1155 { function isApprovedForAll( address owner, address operator - ) external view returns (bool) { + ) public view returns (bool) { return approvals[owner][operator]; } + function isApprovedForId( + uint256 id, + address delegate + ) public view returns (bool) { + // get the owner + address _owner = _getController(tokens[id]).ownerOf(tokens[id]); + return + tokenApprovals[_owner][tokenApprovalsNonce[_owner]][id][delegate]; + } + + function clearAllApprovedForIds(address owner) external { + // make sure the caller is the owner or an approved operator. + require( + msg.sender == owner || approvals[owner][msg.sender], + "L2Registry: caller is not owner or approved operator" + ); + tokenApprovalsNonce[owner]++; + } + function getAuthorization( uint256 id, - address operator - ) public view returns (address owner, bool authorized) { - bytes memory tokenData = tokens[id]; - IController _controller = _getController(tokenData); - owner = _controller.ownerOf(tokenData); - authorized = approvals[owner][operator]; + address delegate + ) public view returns (bool authorized) { + address owner = _getController(tokens[id]).ownerOf(tokens[id]); + authorized = + approvals[owner][delegate] || + tokenApprovals[owner][tokenApprovalsNonce[owner]][id][delegate]; } function supportsInterface( @@ -202,7 +242,9 @@ contract L2Registry is IERC1155 { if (address(oldController) == address(0)) { revert TokenDoesNotExist(id); } - bool operatorApproved = approvals[from][msg.sender]; + bool isApproved = approvals[from][msg.sender] || + tokenApprovals[from][tokenApprovalsNonce[from]][id][msg.sender]; + bytes memory newTokenData = oldController.safeTransferFrom( tokenData, msg.sender, @@ -211,7 +253,7 @@ contract L2Registry is IERC1155 { id, value, data, - operatorApproved + isApproved ); IController newController = _getController(newTokenData); diff --git a/contracts/l2/RootController.sol b/contracts/l2/RootController.sol index 90100ecf9..662a741fa 100644 --- a/contracts/l2/RootController.sol +++ b/contracts/l2/RootController.sol @@ -10,6 +10,9 @@ import "@openzeppelin/contracts/access/Ownable.sol"; contract RootController is Ownable, IController { address resolver; + bytes32 private constant ROOT_NODE = + 0x0000000000000000000000000000000000000000000000000000000000000000; + constructor(address _resolver) Ownable() { resolver = _resolver; } @@ -64,10 +67,16 @@ contract RootController is Ownable, IController { function setSubnode( L2Registry registry, - uint256 node, + uint256 /*node*/, uint256 label, bytes memory subnodeData ) external onlyOwner { - registry.setSubnode(node, label, subnodeData, msg.sender, address(0)); + registry.setSubnode( + uint256(ROOT_NODE), + label, + subnodeData, + msg.sender, + address(0) + ); } } diff --git a/contracts/l2/SimpleController.sol b/contracts/l2/SimpleController.sol index f9c4583bd..38744c8f2 100644 --- a/contracts/l2/SimpleController.sol +++ b/contracts/l2/SimpleController.sol @@ -67,27 +67,29 @@ contract SimpleController is IController { *******************/ function setResolver(uint256 id, address newResolver) external { - (address owner, bool authorized) = registry.getAuthorization( - id, - msg.sender - ); - require(owner == msg.sender || authorized); + // get tokenData + bytes memory tokenData = registry.tokens(id); + (address owner, ) = _unpack(tokenData); + bool isAuthorized = registry.getAuthorization(id, msg.sender); + require(owner == msg.sender || isAuthorized); registry.setNode(id, _pack(owner, newResolver)); } function setSubnode( - uint256 node, + bytes32 node, uint256 label, address subnodeOwner, address subnodeResolver ) external { - (address owner, bool authorized) = registry.getAuthorization( - node, + bytes memory tokenData = registry.tokens(uint256(node)); + (address owner, ) = _unpack(tokenData); + bool isAuthorized = registry.getAuthorization( + uint256(node), msg.sender ); - require(owner == msg.sender || authorized); + require(owner == msg.sender || isAuthorized); registry.setSubnode( - node, + uint256(node), label, _pack(subnodeOwner, subnodeResolver), msg.sender, @@ -96,10 +98,12 @@ contract SimpleController is IController { } function _unpack( - bytes calldata tokenData + bytes memory tokenData ) internal pure returns (address owner, address resolver) { - owner = address(bytes20(tokenData[20:40])); - resolver = address(bytes20(tokenData[40:60])); + assembly { + owner := mload(add(tokenData, 40)) + resolver := mload(add(tokenData, 60)) + } } function _pack( diff --git a/test/l2/TestL2Registry.js b/test/l2/TestL2Registry.js index d255ec02b..ff12aee1d 100644 --- a/test/l2/TestL2Registry.js +++ b/test/l2/TestL2Registry.js @@ -8,7 +8,7 @@ const TEST_NODE = namehash('test') const TEST_SUBNODE = namehash('sub.test') const { deploy } = require('../test-utils/contracts') -contract('L2Registry', function (accounts) { +contract.only('L2Registry', function (accounts) { let signers, deployer, deployerAddress, @@ -17,7 +17,10 @@ contract('L2Registry', function (accounts) { resolver, root, registry, - controller + controller, + dummyAddress, + operator, + delegate beforeEach(async () => { signers = await ethers.getSigners() deployer = await signers[0] @@ -31,13 +34,19 @@ contract('L2Registry', function (accounts) { root = await RootController.new(resolver.address) registry = await L2Registry.new(root.address) controller = await SimpleController.new(registry.address) - }) - it('should deploy', async () => { + + dummyAddress = '0x1234567890123456789012345678901234567890' + operator = signers[3] + delegate = signers[4] + assert.equal(await registry.controller(ROOT_NODE), root.address) + // test to make sure the root node is owned by the deployer + assert.equal(await registry.balanceOf(deployerAddress, ROOT_NODE), 1) + await root.setSubnode( registry.address, - ROOT_NODE, + 0, // This is ignored because the ROOT_NODE is fixed in the root controller. labelhash('test'), ethers.utils.solidityPack( ['address', 'address', 'address'], @@ -47,7 +56,8 @@ contract('L2Registry', function (accounts) { assert.equal(await registry.controller(TEST_NODE), controller.address) assert.equal(await registry.balanceOf(ownerAddress, TEST_NODE), 1) assert.equal(await registry.resolver(TEST_NODE), resolver.address) - + }) + it('should set a subnode on the test node', async () => { await controller.setSubnode( TEST_NODE, labelhash('sub'), @@ -59,4 +69,57 @@ contract('L2Registry', function (accounts) { assert.equal(await registry.balanceOf(subnodeOwnerAddress, TEST_SUBNODE), 1) assert.equal(await registry.resolver(TEST_SUBNODE), resolver.address) }) + + it('should set the resolver', async () => { + await controller.setResolver(TEST_NODE, dummyAddress, { + from: ownerAddress, + }) + assert.equal(await registry.resolver(TEST_NODE), dummyAddress) + }) + + it('should set the resolver as an operator', async () => { + await registry.setApprovalForAll(operator.address, true, { + from: ownerAddress, + }) + await controller.setResolver(TEST_NODE, dummyAddress, { + from: operator.address, + }) + assert.equal(await registry.resolver(TEST_NODE), dummyAddress) + }) + + it('should set the resolver as a delegate', async () => { + await registry.setApprovalForId(delegate.address, TEST_NODE, true, { + from: ownerAddress, + }) + await controller.setResolver(TEST_NODE, dummyAddress, { + from: delegate.address, + }) + assert.equal(await registry.resolver(TEST_NODE), dummyAddress) + }) + + it('should revert if the resolver is set as a delegate after the owner calls clearAllApprovedForIds', async () => { + await registry.setApprovalForId(delegate.address, TEST_NODE, true, { + from: ownerAddress, + }) + await registry.clearAllApprovedForIds(ownerAddress, { from: ownerAddress }) + // make sure the set resolver fails expect revert without a reason + await expect( + controller.setResolver(TEST_NODE, dummyAddress, { + from: delegate.address, + }), + ).to.be.reverted + }) + + // Check to make sure that a operator can call the setApprovalForId function. + it('should set the setApprovalForId as an operator', async () => { + await registry.setApprovalForAll(operator.address, true, { + from: ownerAddress, + }) + + await registry.setApprovalForId(dummyAddress, TEST_NODE, true, { + from: operator.address, + }) + + assert.equal(await registry.isApprovedForId(TEST_NODE, dummyAddress), true) + }) }) From f1a67426c495b9e69175327ced6364b8109d0646 Mon Sep 17 00:00:00 2001 From: nxt3d Date: Thu, 25 Jan 2024 05:49:31 -0500 Subject: [PATCH 02/15] add Record struct with "name" value --- contracts/l2/L2Registry.sol | 62 ++++++++++++++++++++++--------- contracts/l2/SimpleController.sol | 7 ++-- 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/contracts/l2/L2Registry.sol b/contracts/l2/L2Registry.sol index 9204a75b7..935e2de68 100644 --- a/contracts/l2/L2Registry.sol +++ b/contracts/l2/L2Registry.sol @@ -8,7 +8,11 @@ import "@openzeppelin/contracts/utils/Address.sol"; import "./IController.sol"; contract L2Registry is IERC1155 { - mapping(uint256 => bytes) public tokens; + struct Record { + string name; + bytes data; + } + mapping(uint256 => Record) public tokens; mapping(address => mapping(address => bool)) approvals; mapping(address => uint256) tokenApprovalsNonce; mapping(address => mapping(uint256 => mapping(uint256 => mapping(address => bool)))) tokenApprovals; @@ -18,13 +22,21 @@ contract L2Registry is IERC1155 { event NewController(uint256 id, address controller); constructor(bytes memory root) { - tokens[0] = root; + tokens[0].data = root; } /******************** * Public functions * ********************/ + function getData(uint256 id) external view returns (bytes memory) { + return tokens[id].data; + } + + function getName(uint256 id) external view returns (string memory) { + return tokens[id].name; + } + function safeTransferFrom( address from, address to, @@ -62,7 +74,9 @@ contract L2Registry is IERC1155 { bool approved ) external { // get the owner of the token - address _owner = _getController(tokens[id]).ownerOf(tokens[id]); + address _owner = _getController(tokens[id].data).ownerOf( + tokens[id].data + ); // make sure the caller is the owner or an approved operator. require( msg.sender == _owner || isApprovedForAll(_owner, msg.sender), @@ -81,7 +95,7 @@ contract L2Registry is IERC1155 { address owner, uint256 id ) external view returns (uint256) { - bytes memory tokenData = tokens[id]; + bytes memory tokenData = tokens[id].data; IController _controller = _getController(tokenData); if (address(_controller) == address(0)) { revert TokenDoesNotExist(id); @@ -96,7 +110,7 @@ contract L2Registry is IERC1155 { require(owners.length == ids.length); balances = new uint256[](owners.length); for (uint256 i = 0; i < owners.length; i++) { - bytes memory tokenData = tokens[i]; + bytes memory tokenData = tokens[i].data; balances[i] = _getController(tokenData).balanceOf( tokenData, owners[i], @@ -117,7 +131,9 @@ contract L2Registry is IERC1155 { address delegate ) public view returns (bool) { // get the owner - address _owner = _getController(tokens[id]).ownerOf(tokens[id]); + address _owner = _getController(tokens[id].data).ownerOf( + tokens[id].data + ); return tokenApprovals[_owner][tokenApprovalsNonce[_owner]][id][delegate]; } @@ -135,7 +151,19 @@ contract L2Registry is IERC1155 { uint256 id, address delegate ) public view returns (bool authorized) { - address owner = _getController(tokens[id]).ownerOf(tokens[id]); + address owner = _getController(tokens[id].data).ownerOf( + tokens[id].data + ); + authorized = + approvals[owner][delegate] || + tokenApprovals[owner][tokenApprovalsNonce[owner]][id][delegate]; + } + + function getAuthorization( + uint256 id, + address owner, + address delegate + ) public view returns (bool authorized) { authorized = approvals[owner][delegate] || tokenApprovals[owner][tokenApprovalsNonce[owner]][id][delegate]; @@ -150,13 +178,13 @@ contract L2Registry is IERC1155 { } function resolver(uint256 id) external view returns (address) { - bytes memory tokenData = tokens[id]; + bytes memory tokenData = tokens[id].data; IController _controller = _getController(tokenData); return _controller.resolverFor(tokenData); } function controller(uint256 id) external view returns (IController) { - return _getController(tokens[id]); + return _getController(tokens[id].data); } /***************************** @@ -165,7 +193,7 @@ contract L2Registry is IERC1155 { function setNode(uint256 id, bytes memory data) external { // Fetch the current controller for this node - IController oldController = _getController(tokens[id]); + IController oldController = _getController(tokens[id].data); // Only the controller may call this function require(address(oldController) == msg.sender); @@ -176,7 +204,7 @@ contract L2Registry is IERC1155 { } // Update the data for this node. - tokens[id] = data; + tokens[id].data = data; } function setSubnode( @@ -187,14 +215,14 @@ contract L2Registry is IERC1155 { address to ) external { // Fetch the token data and controller for the current node - bytes memory tokenData = tokens[id]; + bytes memory tokenData = tokens[id].data; IController _controller = _getController(tokenData); // Only the controller of the node may call this function require(address(_controller) == msg.sender); // Compute the subnode ID, and fetch the current data for it (if any) uint256 subnode = uint256(keccak256(abi.encodePacked(id, label))); - bytes memory oldSubnodeData = tokens[subnode]; + bytes memory oldSubnodeData = tokens[subnode].data; IController oldSubnodeController = _getController(oldSubnodeData); address oldOwner = oldSubnodeData.length < 20 ? address(0) @@ -206,7 +234,7 @@ contract L2Registry is IERC1155 { emit NewController(subnode, address(newSubnodeController)); } - tokens[subnode] = subnodeData; + tokens[subnode].data = subnodeData; // Fetch the to address, if not supplied, for the TransferSingle event. if (to == address(0) && subnodeData.length >= 20) { @@ -226,7 +254,7 @@ contract L2Registry is IERC1155 { return IController(address(0)); } assembly { - addr := shr(96, mload(add(data, 32))) + addr := mload(add(data, 20)) } } @@ -237,7 +265,7 @@ contract L2Registry is IERC1155 { uint256 value, bytes calldata data ) internal { - bytes memory tokenData = tokens[id]; + bytes memory tokenData = tokens[id].data; IController oldController = _getController(tokenData); if (address(oldController) == address(0)) { revert TokenDoesNotExist(id); @@ -260,6 +288,6 @@ contract L2Registry is IERC1155 { if (newController != oldController) { emit NewController(id, address(newController)); } - tokens[id] = newTokenData; + tokens[id].data = newTokenData; } } diff --git a/contracts/l2/SimpleController.sol b/contracts/l2/SimpleController.sol index 38744c8f2..b4b5f7674 100644 --- a/contracts/l2/SimpleController.sol +++ b/contracts/l2/SimpleController.sol @@ -68,9 +68,9 @@ contract SimpleController is IController { function setResolver(uint256 id, address newResolver) external { // get tokenData - bytes memory tokenData = registry.tokens(id); + bytes memory tokenData = registry.getData(id); (address owner, ) = _unpack(tokenData); - bool isAuthorized = registry.getAuthorization(id, msg.sender); + bool isAuthorized = registry.getAuthorization(id, owner, msg.sender); require(owner == msg.sender || isAuthorized); registry.setNode(id, _pack(owner, newResolver)); } @@ -81,10 +81,11 @@ contract SimpleController is IController { address subnodeOwner, address subnodeResolver ) external { - bytes memory tokenData = registry.tokens(uint256(node)); + bytes memory tokenData = registry.getData(uint256(node)); (address owner, ) = _unpack(tokenData); bool isAuthorized = registry.getAuthorization( uint256(node), + owner, msg.sender ); require(owner == msg.sender || isAuthorized); From 5b551811999179d4ecc8233a31b51e1f5b4047e1 Mon Sep 17 00:00:00 2001 From: nxt3d Date: Thu, 25 Jan 2024 14:50:59 -0500 Subject: [PATCH 03/15] add metadata service and make ownable --- contracts/l2/L2Registry.sol | 21 +++++++++++++++++++-- contracts/l2/SimpleController.sol | 12 ++++++++++++ test/l2/TestL2Registry.js | 30 ++++++++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/contracts/l2/L2Registry.sol b/contracts/l2/L2Registry.sol index 935e2de68..939f8053d 100644 --- a/contracts/l2/L2Registry.sol +++ b/contracts/l2/L2Registry.sol @@ -5,9 +5,12 @@ import "@openzeppelin/contracts/interfaces/IERC1155.sol"; import "@openzeppelin/contracts/interfaces/IERC165.sol"; import "@openzeppelin/contracts/utils/Create2.sol"; import "@openzeppelin/contracts/utils/Address.sol"; +import {IMetadataService} from "../wrapper/IMetadataService.sol"; +import {IERC1155MetadataURI} from "@openzeppelin/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import "./IController.sol"; -contract L2Registry is IERC1155 { +contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { struct Record { string name; bytes data; @@ -17,18 +20,31 @@ contract L2Registry is IERC1155 { mapping(address => uint256) tokenApprovalsNonce; mapping(address => mapping(uint256 => mapping(uint256 => mapping(address => bool)))) tokenApprovals; + IMetadataService public metadataService; + error TokenDoesNotExist(uint256 id); event NewController(uint256 id, address controller); - constructor(bytes memory root) { + constructor(bytes memory root, IMetadataService _metadataService) { tokens[0].data = root; + metadataService = _metadataService; } /******************** * Public functions * ********************/ + function uri(uint256 tokenId) public view returns (string memory) { + return metadataService.uri(tokenId); + } + + function setMetadataService( + IMetadataService _metadataService + ) public onlyOwner { + metadataService = _metadataService; + } + function getData(uint256 id) external view returns (bytes memory) { return tokens[id].data; } @@ -174,6 +190,7 @@ contract L2Registry is IERC1155 { ) external pure returns (bool) { return interfaceId == type(IERC1155).interfaceId || + interfaceId == type(IERC1155MetadataURI).interfaceId || interfaceId == type(IERC165).interfaceId; } diff --git a/contracts/l2/SimpleController.sol b/contracts/l2/SimpleController.sol index b4b5f7674..ea77e29dc 100644 --- a/contracts/l2/SimpleController.sol +++ b/contracts/l2/SimpleController.sol @@ -11,6 +11,9 @@ import "./IController.sol"; * - Byte 0: controller (address) * - Byte 20: owner (address) * - Byte 40: resolver (address) + * _ Byte 60: expiry (uint64) + * - Byte 68: fuses (uint96) + * - Byte 80: renewalController (address) */ contract SimpleController is IController { L2Registry immutable registry; @@ -107,6 +110,15 @@ contract SimpleController is IController { } } + function _getExpiryAndFuses( + bytes memory tokenData + ) internal pure returns (uint64 expiry, uint96 fuses) { + assembly { + expiry := mload(add(tokenData, 68)) + fuses := mload(add(tokenData, 80)) + } + } + function _pack( address owner, address resolver diff --git a/test/l2/TestL2Registry.js b/test/l2/TestL2Registry.js index ff12aee1d..9c404a0cd 100644 --- a/test/l2/TestL2Registry.js +++ b/test/l2/TestL2Registry.js @@ -2,6 +2,7 @@ const L2Registry = artifacts.require('L2Registry.sol') const RootController = artifacts.require('RootController.sol') const DelegatableResolver = artifacts.require('DelegatableResolver.sol') const SimpleController = artifacts.require('SimpleController.sol') +const StaticMetadataService = artifacts.require('StaticMetadataService.sol') const { labelhash, namehash, encodeName, FUSES } = require('../test-utils/ens') const ROOT_NODE = namehash('') const TEST_NODE = namehash('test') @@ -20,7 +21,9 @@ contract.only('L2Registry', function (accounts) { controller, dummyAddress, operator, - delegate + delegate, + metaDataservice + beforeEach(async () => { signers = await ethers.getSigners() deployer = await signers[0] @@ -29,10 +32,15 @@ contract.only('L2Registry', function (accounts) { ownerAddress = await owner.getAddress() subnodeOwner = await signers[2] subnodeOwnerAddress = await subnodeOwner.getAddress() + hacker = await signers[3] + hackerAddress = await hacker.getAddress() + dummyAccount = await signers[4] + dummyAccountAddress = await dummyAccount.getAddress() resolver = await DelegatableResolver.new() + metaDataservice = await StaticMetadataService.new('https://ens.domains') root = await RootController.new(resolver.address) - registry = await L2Registry.new(root.address) + registry = await L2Registry.new(root.address, metaDataservice.address) controller = await SimpleController.new(registry.address) dummyAddress = '0x1234567890123456789012345678901234567890' @@ -57,6 +65,24 @@ contract.only('L2Registry', function (accounts) { assert.equal(await registry.balanceOf(ownerAddress, TEST_NODE), 1) assert.equal(await registry.resolver(TEST_NODE), resolver.address) }) + + it('uri() returns url', async () => { + expect(await registry.uri(123)).to.equal('https://ens.domains') + }) + + it('owner can set a new MetadataService', async () => { + await registry.setMetadataService(dummyAccountAddress) + expect(await registry.metadataService()).to.equal(dummyAccountAddress) + }) + + it('non-owner cannot set a new MetadataService', async () => { + await expect( + registry.setMetadataService(dummyAccountAddress, { + from: hackerAddress, + }), + ).to.be.revertedWith('Ownable: caller is not the owner') + }) + it('should set a subnode on the test node', async () => { await controller.setSubnode( TEST_NODE, From 1a76c9695d085e86cf823887cc89caf251852ed0 Mon Sep 17 00:00:00 2001 From: nxt3d Date: Sat, 27 Jan 2024 21:59:23 -0500 Subject: [PATCH 04/15] add ownerOf function, add expiry, fuses, renewalController, upgrade function --- contracts/l2/IController.sol | 6 +- contracts/l2/IControllerUpgrade.sol | 12 ++ contracts/l2/IFuseController.sol | 12 ++ contracts/l2/L2Registry.sol | 44 ++---- contracts/l2/RootController.sol | 7 +- contracts/l2/SimpleController.sol | 222 ++++++++++++++++++++++++---- test/l2/TestL2Registry.js | 38 ++++- 7 files changed, 276 insertions(+), 65 deletions(-) create mode 100644 contracts/l2/IControllerUpgrade.sol create mode 100644 contracts/l2/IFuseController.sol diff --git a/contracts/l2/IController.sol b/contracts/l2/IController.sol index aac30ef06..b1c55fe95 100644 --- a/contracts/l2/IController.sol +++ b/contracts/l2/IController.sol @@ -3,7 +3,11 @@ pragma solidity ^0.8.17; interface IController { - function ownerOf(bytes calldata tokenData) external view returns (address); + function ownerOfWithData( + bytes calldata tokenData + ) external view returns (address); + + function ownerOf(bytes32 node) external view returns (address); function safeTransferFrom( bytes calldata tokenData, diff --git a/contracts/l2/IControllerUpgrade.sol b/contracts/l2/IControllerUpgrade.sol new file mode 100644 index 000000000..99eacfb2e --- /dev/null +++ b/contracts/l2/IControllerUpgrade.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.17; + +import "./IController.sol"; + +interface IControllerUpgrade is IController { + function upgradeFrom( + bytes32 node, + bytes calldata extraData + ) external returns (bytes memory); +} diff --git a/contracts/l2/IFuseController.sol b/contracts/l2/IFuseController.sol new file mode 100644 index 000000000..5130789dc --- /dev/null +++ b/contracts/l2/IFuseController.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "./IController.sol"; + +interface IFuseController is IController { + function expiryOf(bytes32 node) external view returns (uint64); + + function fusesOf(bytes32 node) external view returns (uint96); + + function renewalControllerOf(bytes32 node) external view returns (address); +} diff --git a/contracts/l2/L2Registry.sol b/contracts/l2/L2Registry.sol index 939f8053d..ffd0f9915 100644 --- a/contracts/l2/L2Registry.sol +++ b/contracts/l2/L2Registry.sol @@ -24,8 +24,6 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { error TokenDoesNotExist(uint256 id); - event NewController(uint256 id, address controller); - constructor(bytes memory root, IMetadataService _metadataService) { tokens[0].data = root; metadataService = _metadataService; @@ -90,7 +88,7 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { bool approved ) external { // get the owner of the token - address _owner = _getController(tokens[id].data).ownerOf( + address _owner = _getController(tokens[id].data).ownerOfWithData( tokens[id].data ); // make sure the caller is the owner or an approved operator. @@ -147,7 +145,7 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { address delegate ) public view returns (bool) { // get the owner - address _owner = _getController(tokens[id].data).ownerOf( + address _owner = _getController(tokens[id].data).ownerOfWithData( tokens[id].data ); return @@ -157,7 +155,7 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { function clearAllApprovedForIds(address owner) external { // make sure the caller is the owner or an approved operator. require( - msg.sender == owner || approvals[owner][msg.sender], + msg.sender == owner || isApprovedForAll(owner, msg.sender), "L2Registry: caller is not owner or approved operator" ); tokenApprovalsNonce[owner]++; @@ -166,11 +164,11 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { function getAuthorization( uint256 id, address delegate - ) public view returns (bool authorized) { - address owner = _getController(tokens[id].data).ownerOf( + ) public view returns (bool /*authorized*/) { + address owner = _getController(tokens[id].data).ownerOfWithData( tokens[id].data ); - authorized = + return approvals[owner][delegate] || tokenApprovals[owner][tokenApprovalsNonce[owner]][id][delegate]; } @@ -179,8 +177,8 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { uint256 id, address owner, address delegate - ) public view returns (bool authorized) { - authorized = + ) public view returns (bool /*authorized*/) { + return approvals[owner][delegate] || tokenApprovals[owner][tokenApprovalsNonce[owner]][id][delegate]; } @@ -194,13 +192,15 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { interfaceId == type(IERC165).interfaceId; } - function resolver(uint256 id) external view returns (address) { + function resolver(uint256 id) external view returns (address /*resolver*/) { bytes memory tokenData = tokens[id].data; IController _controller = _getController(tokenData); return _controller.resolverFor(tokenData); } - function controller(uint256 id) external view returns (IController) { + function controller( + uint256 id + ) external view returns (IController /*controller*/) { return _getController(tokens[id].data); } @@ -214,12 +214,6 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { // Only the controller may call this function require(address(oldController) == msg.sender); - // Fetch the new controller and emit `NewController` if needed. - IController newController = _getController(data); - if (oldController != newController) { - emit NewController(id, address(newController)); - } - // Update the data for this node. tokens[id].data = data; } @@ -243,19 +237,13 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { IController oldSubnodeController = _getController(oldSubnodeData); address oldOwner = oldSubnodeData.length < 20 ? address(0) - : oldSubnodeController.ownerOf(oldSubnodeData); - - // Get the address of the new controller - IController newSubnodeController = _getController(subnodeData); - if (newSubnodeController != oldSubnodeController) { - emit NewController(subnode, address(newSubnodeController)); - } + : oldSubnodeController.ownerOfWithData(oldSubnodeData); tokens[subnode].data = subnodeData; // Fetch the to address, if not supplied, for the TransferSingle event. if (to == address(0) && subnodeData.length >= 20) { - to = _getController(subnodeData).ownerOf(subnodeData); + to = _getController(subnodeData).ownerOfWithData(subnodeData); } emit TransferSingle(operator, oldOwner, to, subnode, 1); } @@ -301,10 +289,6 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { isApproved ); - IController newController = _getController(newTokenData); - if (newController != oldController) { - emit NewController(id, address(newController)); - } tokens[id].data = newTokenData; } } diff --git a/contracts/l2/RootController.sol b/contracts/l2/RootController.sol index 662a741fa..2f47c16c3 100644 --- a/contracts/l2/RootController.sol +++ b/contracts/l2/RootController.sol @@ -24,12 +24,17 @@ contract RootController is Ownable, IController { /************************* * IController functions * *************************/ - function ownerOf( + + function ownerOfWithData( bytes calldata /*tokenData*/ ) external view returns (address) { return owner(); } + function ownerOf(bytes32 /*node*/) external view returns (address) { + return owner(); + } + function safeTransferFrom( bytes calldata /*tokenData*/, address /*sender*/, diff --git a/contracts/l2/SimpleController.sol b/contracts/l2/SimpleController.sol index ea77e29dc..d1ad2411a 100644 --- a/contracts/l2/SimpleController.sol +++ b/contracts/l2/SimpleController.sol @@ -3,7 +3,13 @@ pragma solidity ^0.8.17; import "./L2Registry.sol"; -import "./IController.sol"; +import "./IFuseController.sol"; +import "./IControllerUpgrade.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +error Unauthorised(bytes32 node, address addr); +error CannotUpgrade(); +error nameExpired(bytes32 node); /** * @dev A simple ENS registry controller. Names are permanently owned by a single account. @@ -15,9 +21,20 @@ import "./IController.sol"; * - Byte 68: fuses (uint96) * - Byte 80: renewalController (address) */ -contract SimpleController is IController { +contract SimpleController is Ownable, IFuseController { L2Registry immutable registry; + IControllerUpgrade upgradeContract; + + // A struct to hold the unpacked data + struct TokenData { + address owner; + address resolver; + uint64 expiry; + uint96 fuses; + address renewalController; + } + constructor(L2Registry _registry) { registry = _registry; } @@ -25,8 +42,18 @@ contract SimpleController is IController { /************************* * IController functions * *************************/ - function ownerOf(bytes calldata tokenData) external pure returns (address) { - (address owner, ) = _unpack(tokenData); + + function ownerOfWithData( + bytes calldata tokenData + ) external pure returns (address) { + (address owner, , , , ) = _unpack(tokenData); + return owner; + } + + function ownerOf(bytes32 node) external view returns (address) { + //get the tokenData + bytes memory tokenData = registry.getData(uint256(node)); + (address owner, , , , ) = _unpack(tokenData); return owner; } @@ -40,13 +67,22 @@ contract SimpleController is IController { bytes calldata /*data*/, bool operatorApproved ) external view returns (bytes memory) { - (address owner, address resolver) = _unpack(tokenData); + TokenData memory td; + + ( + td.owner, + td.resolver, + td.expiry, + td.fuses, + td.renewalController + ) = _unpack(tokenData); require(value == 1); - require(from == owner); - require(operator == owner || operatorApproved); + require(from == td.owner); + require(operator == td.owner || operatorApproved); - return _pack(to, resolver); + return + _pack(to, td.resolver, td.expiry, td.fuses, td.renewalController); } function balanceOf( @@ -54,75 +90,203 @@ contract SimpleController is IController { address _owner, uint256 /*id*/ ) external pure returns (uint256) { - (address owner, ) = _unpack(tokenData); + (address owner, , , , ) = _unpack(tokenData); return _owner == owner ? 1 : 0; } function resolverFor( bytes calldata tokenData ) external pure returns (address) { - (, address resolver) = _unpack(tokenData); + (, address resolver, , , ) = _unpack(tokenData); return resolver; } + function expiryOf(bytes32 node) external view returns (uint64) { + // get the tokenData + bytes memory tokenData = registry.getData(uint256(node)); + (, , uint64 expiry, , ) = _unpack(tokenData); + return expiry; + } + + function fusesOf(bytes32 node) external view returns (uint96) { + // get the tokenData + bytes memory tokenData = registry.getData(uint256(node)); + (, , , uint96 fuses, ) = _unpack(tokenData); + return fuses; + } + + function renewalControllerOf(bytes32 node) external view returns (address) { + // get the tokenData + bytes memory tokenData = registry.getData(uint256(node)); + (, , , , address renewalController) = _unpack(tokenData); + return renewalController; + } + + function upgrade(bytes32 node, bytes calldata extraData) public { + // Make sure the upgrade contract is set. + if (address(upgradeContract) == address(0)) { + revert CannotUpgrade(); + } + + // Unpack the tokenData of the node. + bytes memory tokenData = registry.getData(uint256(node)); + ( + address owner, + address resolver, + uint64 expiry, + uint96 fuses, + address renewalController + ) = _unpack(tokenData); + + bool isAuthorized = registry.getAuthorization( + uint256(node), + owner, + msg.sender + ); + + if (owner != msg.sender && !isAuthorized) { + revert Unauthorised(node, msg.sender); + } + + if (!_isExpired(tokenData)) { + revert nameExpired(node); + } + + // Change the controller to the upgrade contract. + registry.setNode( + uint256(node), + _pack( + address(upgradeContract), + resolver, + expiry, + fuses, + renewalController + ) + ); + + // Call the new contract to notify it of the upgrade. + upgradeContract.upgradeFrom(node, extraData); + } + /******************* - * Owner functions * + * Node Owner functions * *******************/ function setResolver(uint256 id, address newResolver) external { // get tokenData bytes memory tokenData = registry.getData(id); - (address owner, ) = _unpack(tokenData); + ( + address owner, + , + uint64 expiry, + uint96 fuses, + address renewalController + ) = _unpack(tokenData); bool isAuthorized = registry.getAuthorization(id, owner, msg.sender); - require(owner == msg.sender || isAuthorized); - registry.setNode(id, _pack(owner, newResolver)); + + if (owner != msg.sender && !isAuthorized) { + revert Unauthorised(bytes32(id), msg.sender); + } + + registry.setNode( + id, + _pack(owner, newResolver, expiry, fuses, renewalController) + ); } function setSubnode( bytes32 node, uint256 label, address subnodeOwner, - address subnodeResolver + address subnodeResolver, + uint64 subnodeExpiry, + uint96 subnodeFuses, + address subnodeRenewalController ) external { bytes memory tokenData = registry.getData(uint256(node)); - (address owner, ) = _unpack(tokenData); + (address owner, , , , ) = _unpack(tokenData); bool isAuthorized = registry.getAuthorization( uint256(node), owner, msg.sender ); - require(owner == msg.sender || isAuthorized); + + if (owner != msg.sender && !isAuthorized) { + revert Unauthorised(node, msg.sender); + } + registry.setSubnode( uint256(node), label, - _pack(subnodeOwner, subnodeResolver), + _pack( + subnodeOwner, + subnodeResolver, + subnodeExpiry, + subnodeFuses, + subnodeRenewalController + ), msg.sender, subnodeOwner ); } + /******************* + * Owner only functions * + *******************/ + + // A function that sets the upgrade contract. + function setUpgradeContract( + IControllerUpgrade _upgradeContract + ) external onlyOwner { + upgradeContract = _upgradeContract; + } + + /********************** + * Internal functions * + **********************/ + + function _isExpired(bytes memory tokenData) internal view returns (bool) { + (, , uint64 expiry, , ) = _unpack(tokenData); + return expiry <= block.timestamp; + } + function _unpack( bytes memory tokenData - ) internal pure returns (address owner, address resolver) { + ) + internal + pure + returns ( + address owner, + address resolver, + uint64 expiry, + uint96 fuses, + address renewalController + ) + { assembly { owner := mload(add(tokenData, 40)) resolver := mload(add(tokenData, 60)) - } - } - - function _getExpiryAndFuses( - bytes memory tokenData - ) internal pure returns (uint64 expiry, uint96 fuses) { - assembly { expiry := mload(add(tokenData, 68)) fuses := mload(add(tokenData, 80)) + renewalController := mload(add(tokenData, 92)) } } function _pack( address owner, - address resolver - ) internal view returns (bytes memory tokenData) { - tokenData = abi.encodePacked(address(this), owner, resolver); + address resolver, + uint64 expiry, + uint96 fuses, + address renewalController + ) internal view returns (bytes memory /*tokenData*/) { + return + abi.encodePacked( + address(this), + owner, + resolver, + expiry, + fuses, + renewalController + ); } } diff --git a/test/l2/TestL2Registry.js b/test/l2/TestL2Registry.js index 9c404a0cd..a5d10b2f7 100644 --- a/test/l2/TestL2Registry.js +++ b/test/l2/TestL2Registry.js @@ -8,6 +8,12 @@ const ROOT_NODE = namehash('') const TEST_NODE = namehash('test') const TEST_SUBNODE = namehash('sub.test') const { deploy } = require('../test-utils/contracts') +const { EMPTY_BYTES32, EMPTY_ADDRESS } = require('../test-utils/constants') + +// The maximum value of a uint64 is 2^64 - 1 = 18446744073709551615 +// use BN instead of BigNumber to avoid BN error + +const MAX_UINT64 = '18446744073709551615' contract.only('L2Registry', function (accounts) { let signers, @@ -52,18 +58,32 @@ contract.only('L2Registry', function (accounts) { // test to make sure the root node is owned by the deployer assert.equal(await registry.balanceOf(deployerAddress, ROOT_NODE), 1) + const packedData = ethers.utils.solidityPack( + ['address', 'address', 'address', 'uint64', 'uint32', 'address'], + [ + controller.address, + ownerAddress, + resolver.address, + MAX_UINT64, + 0, + EMPTY_ADDRESS, + ], + ) + await root.setSubnode( registry.address, 0, // This is ignored because the ROOT_NODE is fixed in the root controller. labelhash('test'), - ethers.utils.solidityPack( - ['address', 'address', 'address'], - [controller.address, ownerAddress, resolver.address], - ), + packedData, ) + assert.equal(await registry.controller(TEST_NODE), controller.address) assert.equal(await registry.balanceOf(ownerAddress, TEST_NODE), 1) assert.equal(await registry.resolver(TEST_NODE), resolver.address) + assert.equal(await controller.ownerOf(TEST_NODE), ownerAddress) + assert.equal(await controller.expiryOf(TEST_NODE), MAX_UINT64) + assert.equal(await controller.fusesOf(TEST_NODE), 0) + assert.equal(await controller.renewalControllerOf(TEST_NODE), EMPTY_ADDRESS) }) it('uri() returns url', async () => { @@ -89,11 +109,21 @@ contract.only('L2Registry', function (accounts) { labelhash('sub'), subnodeOwnerAddress, resolver.address, + 5184000, // 60 days + 0, // no fuse + EMPTY_ADDRESS, // no controller { from: ownerAddress }, ) assert.equal(await registry.controller(TEST_SUBNODE), controller.address) assert.equal(await registry.balanceOf(subnodeOwnerAddress, TEST_SUBNODE), 1) assert.equal(await registry.resolver(TEST_SUBNODE), resolver.address) + assert.equal(await controller.ownerOf(TEST_SUBNODE), subnodeOwnerAddress) + assert.equal(await controller.expiryOf(TEST_SUBNODE), 5184000) + assert.equal(await controller.fusesOf(TEST_SUBNODE), 0) + assert.equal( + await controller.renewalControllerOf(TEST_SUBNODE), + EMPTY_ADDRESS, + ) }) it('should set the resolver', async () => { From 90ba244a364d699a629e7a096c77f184b92873f9 Mon Sep 17 00:00:00 2001 From: nxt3d Date: Sat, 27 Jan 2024 22:05:35 -0500 Subject: [PATCH 05/15] change SimpleController to FuseController --- contracts/l2/{SimpleController.sol => FuseController.sol} | 2 +- test/l2/TestL2Registry.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename contracts/l2/{SimpleController.sol => FuseController.sol} (99%) diff --git a/contracts/l2/SimpleController.sol b/contracts/l2/FuseController.sol similarity index 99% rename from contracts/l2/SimpleController.sol rename to contracts/l2/FuseController.sol index d1ad2411a..469f18b77 100644 --- a/contracts/l2/SimpleController.sol +++ b/contracts/l2/FuseController.sol @@ -21,7 +21,7 @@ error nameExpired(bytes32 node); * - Byte 68: fuses (uint96) * - Byte 80: renewalController (address) */ -contract SimpleController is Ownable, IFuseController { +contract FuseController is Ownable, IFuseController { L2Registry immutable registry; IControllerUpgrade upgradeContract; diff --git a/test/l2/TestL2Registry.js b/test/l2/TestL2Registry.js index a5d10b2f7..8430fcd45 100644 --- a/test/l2/TestL2Registry.js +++ b/test/l2/TestL2Registry.js @@ -1,7 +1,7 @@ const L2Registry = artifacts.require('L2Registry.sol') const RootController = artifacts.require('RootController.sol') const DelegatableResolver = artifacts.require('DelegatableResolver.sol') -const SimpleController = artifacts.require('SimpleController.sol') +const FuseController = artifacts.require('FuseController.sol') const StaticMetadataService = artifacts.require('StaticMetadataService.sol') const { labelhash, namehash, encodeName, FUSES } = require('../test-utils/ens') const ROOT_NODE = namehash('') @@ -47,7 +47,7 @@ contract.only('L2Registry', function (accounts) { metaDataservice = await StaticMetadataService.new('https://ens.domains') root = await RootController.new(resolver.address) registry = await L2Registry.new(root.address, metaDataservice.address) - controller = await SimpleController.new(registry.address) + controller = await FuseController.new(registry.address) dummyAddress = '0x1234567890123456789012345678901234567890' operator = signers[3] From 570323c1cffbadf555d7f3834d978bc96b29bf8a Mon Sep 17 00:00:00 2001 From: nxt3d Date: Sun, 28 Jan 2024 11:31:21 -0500 Subject: [PATCH 06/15] Add upgrade test --- contracts/l2/FuseController.sol | 27 +- contracts/l2/IControllerUpgrade.sol | 5 +- contracts/l2/IFuseController.sol | 7 + contracts/l2/L2Registry.sol | 1 + contracts/l2/mocks/FuseControllerUpgraded.sol | 302 ++++++++++++++++++ test/l2/TestL2Registry.js | 36 +++ 6 files changed, 369 insertions(+), 9 deletions(-) create mode 100644 contracts/l2/mocks/FuseControllerUpgraded.sol diff --git a/contracts/l2/FuseController.sol b/contracts/l2/FuseController.sol index 469f18b77..b3e16ab90 100644 --- a/contracts/l2/FuseController.sol +++ b/contracts/l2/FuseController.sol @@ -82,7 +82,14 @@ contract FuseController is Ownable, IFuseController { require(operator == td.owner || operatorApproved); return - _pack(to, td.resolver, td.expiry, td.fuses, td.renewalController); + _pack( + address(this), + to, + td.resolver, + td.expiry, + td.fuses, + td.renewalController + ); } function balanceOf( @@ -148,7 +155,7 @@ contract FuseController is Ownable, IFuseController { revert Unauthorised(node, msg.sender); } - if (!_isExpired(tokenData)) { + if (_isExpired(tokenData)) { revert nameExpired(node); } @@ -157,6 +164,7 @@ contract FuseController is Ownable, IFuseController { uint256(node), _pack( address(upgradeContract), + owner, resolver, expiry, fuses, @@ -190,7 +198,14 @@ contract FuseController is Ownable, IFuseController { registry.setNode( id, - _pack(owner, newResolver, expiry, fuses, renewalController) + _pack( + address(this), + owner, + newResolver, + expiry, + fuses, + renewalController + ) ); } @@ -219,6 +234,7 @@ contract FuseController is Ownable, IFuseController { uint256(node), label, _pack( + address(this), subnodeOwner, subnodeResolver, subnodeExpiry, @@ -235,7 +251,7 @@ contract FuseController is Ownable, IFuseController { *******************/ // A function that sets the upgrade contract. - function setUpgradeContract( + function setUpgradeController( IControllerUpgrade _upgradeContract ) external onlyOwner { upgradeContract = _upgradeContract; @@ -273,6 +289,7 @@ contract FuseController is Ownable, IFuseController { } function _pack( + address controller, address owner, address resolver, uint64 expiry, @@ -281,7 +298,7 @@ contract FuseController is Ownable, IFuseController { ) internal view returns (bytes memory /*tokenData*/) { return abi.encodePacked( - address(this), + controller, owner, resolver, expiry, diff --git a/contracts/l2/IControllerUpgrade.sol b/contracts/l2/IControllerUpgrade.sol index 99eacfb2e..e4770da3f 100644 --- a/contracts/l2/IControllerUpgrade.sol +++ b/contracts/l2/IControllerUpgrade.sol @@ -5,8 +5,5 @@ pragma solidity ^0.8.17; import "./IController.sol"; interface IControllerUpgrade is IController { - function upgradeFrom( - bytes32 node, - bytes calldata extraData - ) external returns (bytes memory); + function upgradeFrom(bytes32 node, bytes calldata extraData) external; } diff --git a/contracts/l2/IFuseController.sol b/contracts/l2/IFuseController.sol index 5130789dc..ebcecfbdf 100644 --- a/contracts/l2/IFuseController.sol +++ b/contracts/l2/IFuseController.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.17; import "./IController.sol"; +import "./IControllerUpgrade.sol"; interface IFuseController is IController { function expiryOf(bytes32 node) external view returns (uint64); @@ -9,4 +10,10 @@ interface IFuseController is IController { function fusesOf(bytes32 node) external view returns (uint96); function renewalControllerOf(bytes32 node) external view returns (address); + + function upgrade(bytes32 node, bytes calldata extraData) external; + + function setUpgradeController( + IControllerUpgrade _upgradeController + ) external; } diff --git a/contracts/l2/L2Registry.sol b/contracts/l2/L2Registry.sol index ffd0f9915..87735ac5a 100644 --- a/contracts/l2/L2Registry.sol +++ b/contracts/l2/L2Registry.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; + import "@openzeppelin/contracts/interfaces/IERC1155.sol"; import "@openzeppelin/contracts/interfaces/IERC165.sol"; import "@openzeppelin/contracts/utils/Create2.sol"; diff --git a/contracts/l2/mocks/FuseControllerUpgraded.sol b/contracts/l2/mocks/FuseControllerUpgraded.sol new file mode 100644 index 000000000..07e7ff8cb --- /dev/null +++ b/contracts/l2/mocks/FuseControllerUpgraded.sol @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.17; + +import "../L2Registry.sol"; +import "../IFuseController.sol"; +import "../IControllerUpgrade.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +import "hardhat/console.sol"; + +error Unauthorised(bytes32 node, address addr); +error CannotUpgrade(); +error nameExpired(bytes32 node); + +/** + * @dev A simple ENS registry controller. Names are permanently owned by a single account. + * Name data is structured as follows: + * - Byte 0: controller (address) + * - Byte 20: owner (address) + * - Byte 40: resolver (address) + * _ Byte 60: expiry (uint64) + * - Byte 68: fuses (uint96) + * - Byte 80: renewalController (address) + */ +contract FuseControllerUpgraded is + Ownable, + IFuseController, + IControllerUpgrade +{ + L2Registry immutable registry; + + IControllerUpgrade upgradeContract; + + // A struct to hold the unpacked data + struct TokenData { + address owner; + address resolver; + uint64 expiry; + uint96 fuses; + address renewalController; + } + + constructor(L2Registry _registry) { + registry = _registry; + } + + /************************* + * IController functions * + *************************/ + + function ownerOfWithData( + bytes calldata tokenData + ) external pure returns (address) { + (address owner, , , , ) = _unpack(tokenData); + return owner; + } + + function ownerOf(bytes32 node) external view returns (address) { + //get the tokenData + bytes memory tokenData = registry.getData(uint256(node)); + console.logBytes(tokenData); + (address owner, , , , ) = _unpack(tokenData); + return owner; + } + + function safeTransferFrom( + bytes calldata tokenData, + address operator, + address from, + address to, + uint256 /*id*/, + uint256 value, + bytes calldata /*data*/, + bool operatorApproved + ) external view returns (bytes memory) { + TokenData memory td; + + ( + td.owner, + td.resolver, + td.expiry, + td.fuses, + td.renewalController + ) = _unpack(tokenData); + + require(value == 1); + require(from == td.owner); + require(operator == td.owner || operatorApproved); + + return + _pack(to, td.resolver, td.expiry, td.fuses, td.renewalController); + } + + function balanceOf( + bytes calldata tokenData, + address _owner, + uint256 /*id*/ + ) external pure returns (uint256) { + (address owner, , , , ) = _unpack(tokenData); + return _owner == owner ? 1 : 0; + } + + function resolverFor( + bytes calldata tokenData + ) external pure returns (address) { + (, address resolver, , , ) = _unpack(tokenData); + return resolver; + } + + function expiryOf(bytes32 node) external view returns (uint64) { + // get the tokenData + bytes memory tokenData = registry.getData(uint256(node)); + (, , uint64 expiry, , ) = _unpack(tokenData); + return expiry; + } + + function fusesOf(bytes32 node) external view returns (uint96) { + // get the tokenData + bytes memory tokenData = registry.getData(uint256(node)); + (, , , uint96 fuses, ) = _unpack(tokenData); + return fuses; + } + + function renewalControllerOf(bytes32 node) external view returns (address) { + // get the tokenData + bytes memory tokenData = registry.getData(uint256(node)); + (, , , , address renewalController) = _unpack(tokenData); + return renewalController; + } + + function upgrade(bytes32 node, bytes calldata extraData) public { + // Make sure the upgrade contract is set. + if (address(upgradeContract) == address(0)) { + revert CannotUpgrade(); + } + + // Unpack the tokenData of the node. + bytes memory tokenData = registry.getData(uint256(node)); + ( + address owner, + address resolver, + uint64 expiry, + uint96 fuses, + address renewalController + ) = _unpack(tokenData); + + bool isAuthorized = registry.getAuthorization( + uint256(node), + owner, + msg.sender + ); + + if (owner != msg.sender && !isAuthorized) { + revert Unauthorised(node, msg.sender); + } + + if (!_isExpired(tokenData)) { + revert nameExpired(node); + } + + // Change the controller to the upgrade contract. + registry.setNode( + uint256(node), + _pack( + address(upgradeContract), + resolver, + expiry, + fuses, + renewalController + ) + ); + + upgradeContract.upgradeFrom(node, extraData); + } + + function upgradeFrom(bytes32 node, bytes calldata extraData) external { + // we don't need to do anything here. + } + + /******************* + * Node Owner functions * + *******************/ + + function setResolver(uint256 id, address newResolver) external { + // get tokenData + bytes memory tokenData = registry.getData(id); + ( + address owner, + , + uint64 expiry, + uint96 fuses, + address renewalController + ) = _unpack(tokenData); + bool isAuthorized = registry.getAuthorization(id, owner, msg.sender); + + if (owner != msg.sender && !isAuthorized) { + revert Unauthorised(bytes32(id), msg.sender); + } + + registry.setNode( + id, + _pack(owner, newResolver, expiry, fuses, renewalController) + ); + } + + function setSubnode( + bytes32 node, + uint256 label, + address subnodeOwner, + address subnodeResolver, + uint64 subnodeExpiry, + uint96 subnodeFuses, + address subnodeRenewalController + ) external { + bytes memory tokenData = registry.getData(uint256(node)); + (address owner, , , , ) = _unpack(tokenData); + bool isAuthorized = registry.getAuthorization( + uint256(node), + owner, + msg.sender + ); + + if (owner != msg.sender && !isAuthorized) { + revert Unauthorised(node, msg.sender); + } + + registry.setSubnode( + uint256(node), + label, + _pack( + subnodeOwner, + subnodeResolver, + subnodeExpiry, + subnodeFuses, + subnodeRenewalController + ), + msg.sender, + subnodeOwner + ); + } + + /******************* + * Owner only functions * + *******************/ + + // A function that sets the upgrade contract. + function setUpgradeController( + IControllerUpgrade _upgradeContract + ) external onlyOwner { + upgradeContract = _upgradeContract; + } + + /********************** + * Internal functions * + **********************/ + + function _isExpired(bytes memory tokenData) internal view returns (bool) { + (, , uint64 expiry, , ) = _unpack(tokenData); + return expiry <= block.timestamp; + } + + function _unpack( + bytes memory tokenData + ) + internal + pure + returns ( + address owner, + address resolver, + uint64 expiry, + uint96 fuses, + address renewalController + ) + { + assembly { + owner := mload(add(tokenData, 40)) + resolver := mload(add(tokenData, 60)) + expiry := mload(add(tokenData, 68)) + fuses := mload(add(tokenData, 80)) + renewalController := mload(add(tokenData, 92)) + } + } + + function _pack( + address owner, + address resolver, + uint64 expiry, + uint96 fuses, + address renewalController + ) internal view returns (bytes memory /*tokenData*/) { + return + abi.encodePacked( + address(this), + owner, + resolver, + expiry, + fuses, + renewalController + ); + } +} diff --git a/test/l2/TestL2Registry.js b/test/l2/TestL2Registry.js index 8430fcd45..41a7d9572 100644 --- a/test/l2/TestL2Registry.js +++ b/test/l2/TestL2Registry.js @@ -2,6 +2,7 @@ const L2Registry = artifacts.require('L2Registry.sol') const RootController = artifacts.require('RootController.sol') const DelegatableResolver = artifacts.require('DelegatableResolver.sol') const FuseController = artifacts.require('FuseController.sol') +const FuseControllerUpgraded = artifacts.require('FuseControllerUpgraded.sol') const StaticMetadataService = artifacts.require('StaticMetadataService.sol') const { labelhash, namehash, encodeName, FUSES } = require('../test-utils/ens') const ROOT_NODE = namehash('') @@ -25,6 +26,7 @@ contract.only('L2Registry', function (accounts) { root, registry, controller, + controllerUpgraded, dummyAddress, operator, delegate, @@ -48,6 +50,7 @@ contract.only('L2Registry', function (accounts) { root = await RootController.new(resolver.address) registry = await L2Registry.new(root.address, metaDataservice.address) controller = await FuseController.new(registry.address) + controllerUpgraded = await FuseControllerUpgraded.new(registry.address) dummyAddress = '0x1234567890123456789012345678901234567890' operator = signers[3] @@ -178,4 +181,37 @@ contract.only('L2Registry', function (accounts) { assert.equal(await registry.isApprovedForId(TEST_NODE, dummyAddress), true) }) + + // Check to make sure we can upgrade the controller + it('should upgrade the controller', async () => { + // get the controller + const currentController = await registry.controller(TEST_NODE) + // set the upgraded controller on the controller. + await controller.setUpgradeController(controllerUpgraded.address, { + from: deployerAddress, + }) + + // upgrade the controller of the TEST_NODE using the upgrade(node, extraData) function + await controller.upgrade(TEST_NODE, '0x', { + from: ownerAddress, + }) + + // get the new controller + const _upgradedController = await registry.controller(TEST_NODE) + + // check to make sure the controller is the upgraded controller + assert.equal(_upgradedController, controllerUpgraded.address) + + // create am instace from the upgraded controller's address + _upgradedControllerInstance = await ethers.getContractAt( + 'FuseControllerUpgraded', + _upgradedController, + ) + + // check to make sure the owner is the same on the upgraded controller + assert.equal( + await _upgradedControllerInstance.ownerOf(TEST_NODE), + ownerAddress, + ) + }) }) From e13942157fb69bc484463ea1d2a588c5517d2119 Mon Sep 17 00:00:00 2001 From: nxt3d Date: Mon, 29 Jan 2024 16:35:17 -0500 Subject: [PATCH 07/15] add safe transfer acceptance checking --- contracts/l2/FuseController.sol | 6 +- ...grade.sol => IControllerUpgradeTarget.sol} | 2 +- contracts/l2/IFuseController.sol | 4 +- contracts/l2/L2Registry.sol | 101 ++++++++++++++++++ contracts/l2/mocks/FuseControllerUpgraded.sol | 6 +- 5 files changed, 110 insertions(+), 9 deletions(-) rename contracts/l2/{IControllerUpgrade.sol => IControllerUpgradeTarget.sol} (76%) diff --git a/contracts/l2/FuseController.sol b/contracts/l2/FuseController.sol index b3e16ab90..bccfa9049 100644 --- a/contracts/l2/FuseController.sol +++ b/contracts/l2/FuseController.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.17; import "./L2Registry.sol"; import "./IFuseController.sol"; -import "./IControllerUpgrade.sol"; +import "./IControllerUpgradeTarget.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; error Unauthorised(bytes32 node, address addr); @@ -24,7 +24,7 @@ error nameExpired(bytes32 node); contract FuseController is Ownable, IFuseController { L2Registry immutable registry; - IControllerUpgrade upgradeContract; + IControllerUpgradeTarget upgradeContract; // A struct to hold the unpacked data struct TokenData { @@ -252,7 +252,7 @@ contract FuseController is Ownable, IFuseController { // A function that sets the upgrade contract. function setUpgradeController( - IControllerUpgrade _upgradeContract + IControllerUpgradeTarget _upgradeContract ) external onlyOwner { upgradeContract = _upgradeContract; } diff --git a/contracts/l2/IControllerUpgrade.sol b/contracts/l2/IControllerUpgradeTarget.sol similarity index 76% rename from contracts/l2/IControllerUpgrade.sol rename to contracts/l2/IControllerUpgradeTarget.sol index e4770da3f..49b0bb0bc 100644 --- a/contracts/l2/IControllerUpgrade.sol +++ b/contracts/l2/IControllerUpgradeTarget.sol @@ -4,6 +4,6 @@ pragma solidity ^0.8.17; import "./IController.sol"; -interface IControllerUpgrade is IController { +interface IControllerUpgradeTarget is IController { function upgradeFrom(bytes32 node, bytes calldata extraData) external; } diff --git a/contracts/l2/IFuseController.sol b/contracts/l2/IFuseController.sol index ebcecfbdf..ec61770bc 100644 --- a/contracts/l2/IFuseController.sol +++ b/contracts/l2/IFuseController.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.17; import "./IController.sol"; -import "./IControllerUpgrade.sol"; +import "./IControllerUpgradeTarget.sol"; interface IFuseController is IController { function expiryOf(bytes32 node) external view returns (uint64); @@ -14,6 +14,6 @@ interface IFuseController is IController { function upgrade(bytes32 node, bytes calldata extraData) external; function setUpgradeController( - IControllerUpgrade _upgradeController + IControllerUpgradeTarget _upgradeController ) external; } diff --git a/contracts/l2/L2Registry.sol b/contracts/l2/L2Registry.sol index 87735ac5a..4cfa348e8 100644 --- a/contracts/l2/L2Registry.sol +++ b/contracts/l2/L2Registry.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.17; import "@openzeppelin/contracts/interfaces/IERC1155.sol"; +import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import "@openzeppelin/contracts/interfaces/IERC165.sol"; import "@openzeppelin/contracts/utils/Create2.sol"; import "@openzeppelin/contracts/utils/Address.sol"; @@ -12,6 +13,8 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import "./IController.sol"; contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { + using Address for address; + struct Record { string name; bytes data; @@ -25,6 +28,8 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { error TokenDoesNotExist(uint256 id); + event NewController(uint256 id, address controller); + constructor(bytes memory root, IMetadataService _metadataService) { tokens[0].data = root; metadataService = _metadataService; @@ -61,6 +66,8 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { ) external { _safeTransferFrom(from, to, id, value, data); emit TransferSingle(msg.sender, from, to, id, value); + + _doSafeTransferAcceptanceCheck(msg.sender, from, to, id, value, data); } function safeBatchTransferFrom( @@ -75,6 +82,15 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { _safeTransferFrom(from, to, ids[i], values[i], data); } emit TransferBatch(msg.sender, from, to, ids, values); + + _doSafeBatchTransferAcceptanceCheck( + msg.sender, + from, + to, + ids, + values, + data + ); } function setApprovalForAll(address operator, bool approved) external { @@ -215,6 +231,12 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { // Only the controller may call this function require(address(oldController) == msg.sender); + // Fetch the new controller and emit `NewController` if needed. + IController newController = _getController(data); + if (oldController != newController) { + emit NewController(id, address(newController)); + } + // Update the data for this node. tokens[id].data = data; } @@ -240,13 +262,29 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { ? address(0) : oldSubnodeController.ownerOfWithData(oldSubnodeData); + // Get the address of the new controller + IController newSubnodeController = _getController(subnodeData); + if (newSubnodeController != oldSubnodeController) { + emit NewController(subnode, address(newSubnodeController)); + } + tokens[subnode].data = subnodeData; // Fetch the to address, if not supplied, for the TransferSingle event. if (to == address(0) && subnodeData.length >= 20) { to = _getController(subnodeData).ownerOfWithData(subnodeData); } + emit TransferSingle(operator, oldOwner, to, subnode, 1); + + _doSafeTransferAcceptanceCheck( + operator, + oldOwner, + to, + subnode, + 1, + bytes("") + ); } /********************** @@ -292,4 +330,67 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { tokens[id].data = newTokenData; } + + function _doSafeTransferAcceptanceCheck( + address operator, + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) private { + if (to.isContract()) { + try + IERC1155Receiver(to).onERC1155Received( + operator, + from, + id, + amount, + data + ) + returns (bytes4 response) { + if ( + response != IERC1155Receiver(to).onERC1155Received.selector + ) { + revert("ERC1155: ERC1155Receiver rejected tokens"); + } + } catch Error(string memory reason) { + revert(reason); + } catch { + revert("ERC1155: transfer to non ERC1155Receiver implementer"); + } + } + } + + function _doSafeBatchTransferAcceptanceCheck( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) private { + if (to.isContract()) { + try + IERC1155Receiver(to).onERC1155BatchReceived( + operator, + from, + ids, + amounts, + data + ) + returns (bytes4 response) { + if ( + response != + IERC1155Receiver(to).onERC1155BatchReceived.selector + ) { + revert("ERC1155: ERC1155Receiver rejected tokens"); + } + } catch Error(string memory reason) { + revert(reason); + } catch { + revert("ERC1155: transfer to non ERC1155Receiver implementer"); + } + } + } } diff --git a/contracts/l2/mocks/FuseControllerUpgraded.sol b/contracts/l2/mocks/FuseControllerUpgraded.sol index 07e7ff8cb..6db713316 100644 --- a/contracts/l2/mocks/FuseControllerUpgraded.sol +++ b/contracts/l2/mocks/FuseControllerUpgraded.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.17; import "../L2Registry.sol"; import "../IFuseController.sol"; -import "../IControllerUpgrade.sol"; +import "../IControllerUpgradeTarget.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "hardhat/console.sol"; @@ -26,11 +26,11 @@ error nameExpired(bytes32 node); contract FuseControllerUpgraded is Ownable, IFuseController, - IControllerUpgrade + IControllerUpgradeTarget { L2Registry immutable registry; - IControllerUpgrade upgradeContract; + IControllerUpgradeTarget upgradeContract; // A struct to hold the unpacked data struct TokenData { From 83d43da74e812e30ee3fe4e1ae57dd6d748af997 Mon Sep 17 00:00:00 2001 From: nxt3d Date: Wed, 14 Feb 2024 06:28:36 -0500 Subject: [PATCH 08/15] change fuses from uint96 to uint64 --- contracts/l2/FuseController.sol | 32 ++++++++++--------- contracts/l2/IFuseController.sol | 2 +- contracts/l2/mocks/FuseControllerUpgraded.sol | 20 ++++++------ test/l2/TestL2Registry.js | 2 +- 4 files changed, 29 insertions(+), 27 deletions(-) diff --git a/contracts/l2/FuseController.sol b/contracts/l2/FuseController.sol index bccfa9049..1d248a1c9 100644 --- a/contracts/l2/FuseController.sol +++ b/contracts/l2/FuseController.sol @@ -18,7 +18,7 @@ error nameExpired(bytes32 node); * - Byte 20: owner (address) * - Byte 40: resolver (address) * _ Byte 60: expiry (uint64) - * - Byte 68: fuses (uint96) + * - Byte 68: fuses (uint64) * - Byte 80: renewalController (address) */ contract FuseController is Ownable, IFuseController { @@ -31,7 +31,7 @@ contract FuseController is Ownable, IFuseController { address owner; address resolver; uint64 expiry; - uint96 fuses; + uint64 fuses; address renewalController; } @@ -45,7 +45,7 @@ contract FuseController is Ownable, IFuseController { function ownerOfWithData( bytes calldata tokenData - ) external pure returns (address) { + ) external view returns (address) { (address owner, , , , ) = _unpack(tokenData); return owner; } @@ -96,14 +96,14 @@ contract FuseController is Ownable, IFuseController { bytes calldata tokenData, address _owner, uint256 /*id*/ - ) external pure returns (uint256) { + ) external view returns (uint256) { (address owner, , , , ) = _unpack(tokenData); return _owner == owner ? 1 : 0; } function resolverFor( bytes calldata tokenData - ) external pure returns (address) { + ) external view returns (address) { (, address resolver, , , ) = _unpack(tokenData); return resolver; } @@ -115,10 +115,10 @@ contract FuseController is Ownable, IFuseController { return expiry; } - function fusesOf(bytes32 node) external view returns (uint96) { + function fusesOf(bytes32 node) external view returns (uint64) { // get the tokenData bytes memory tokenData = registry.getData(uint256(node)); - (, , , uint96 fuses, ) = _unpack(tokenData); + (, , , uint64 fuses, ) = _unpack(tokenData); return fuses; } @@ -141,7 +141,7 @@ contract FuseController is Ownable, IFuseController { address owner, address resolver, uint64 expiry, - uint96 fuses, + uint64 fuses, address renewalController ) = _unpack(tokenData); @@ -187,7 +187,7 @@ contract FuseController is Ownable, IFuseController { address owner, , uint64 expiry, - uint96 fuses, + uint64 fuses, address renewalController ) = _unpack(tokenData); bool isAuthorized = registry.getAuthorization(id, owner, msg.sender); @@ -215,7 +215,7 @@ contract FuseController is Ownable, IFuseController { address subnodeOwner, address subnodeResolver, uint64 subnodeExpiry, - uint96 subnodeFuses, + uint64 subnodeFuses, address subnodeRenewalController ) external { bytes memory tokenData = registry.getData(uint256(node)); @@ -270,21 +270,23 @@ contract FuseController is Ownable, IFuseController { bytes memory tokenData ) internal - pure + view returns ( address owner, address resolver, uint64 expiry, - uint96 fuses, + uint64 fuses, address renewalController ) { + require(tokenData.length == 96, "Invalid tokenData length"); + assembly { owner := mload(add(tokenData, 40)) resolver := mload(add(tokenData, 60)) expiry := mload(add(tokenData, 68)) - fuses := mload(add(tokenData, 80)) - renewalController := mload(add(tokenData, 92)) + fuses := mload(add(tokenData, 76)) + renewalController := mload(add(tokenData, 96)) } } @@ -293,7 +295,7 @@ contract FuseController is Ownable, IFuseController { address owner, address resolver, uint64 expiry, - uint96 fuses, + uint64 fuses, address renewalController ) internal view returns (bytes memory /*tokenData*/) { return diff --git a/contracts/l2/IFuseController.sol b/contracts/l2/IFuseController.sol index ec61770bc..ec5a11e58 100644 --- a/contracts/l2/IFuseController.sol +++ b/contracts/l2/IFuseController.sol @@ -7,7 +7,7 @@ import "./IControllerUpgradeTarget.sol"; interface IFuseController is IController { function expiryOf(bytes32 node) external view returns (uint64); - function fusesOf(bytes32 node) external view returns (uint96); + function fusesOf(bytes32 node) external view returns (uint64); function renewalControllerOf(bytes32 node) external view returns (address); diff --git a/contracts/l2/mocks/FuseControllerUpgraded.sol b/contracts/l2/mocks/FuseControllerUpgraded.sol index 6db713316..72c662712 100644 --- a/contracts/l2/mocks/FuseControllerUpgraded.sol +++ b/contracts/l2/mocks/FuseControllerUpgraded.sol @@ -20,7 +20,7 @@ error nameExpired(bytes32 node); * - Byte 20: owner (address) * - Byte 40: resolver (address) * _ Byte 60: expiry (uint64) - * - Byte 68: fuses (uint96) + * - Byte 68: fuses (uint64) * - Byte 80: renewalController (address) */ contract FuseControllerUpgraded is @@ -37,7 +37,7 @@ contract FuseControllerUpgraded is address owner; address resolver; uint64 expiry; - uint96 fuses; + uint64 fuses; address renewalController; } @@ -115,10 +115,10 @@ contract FuseControllerUpgraded is return expiry; } - function fusesOf(bytes32 node) external view returns (uint96) { + function fusesOf(bytes32 node) external view returns (uint64) { // get the tokenData bytes memory tokenData = registry.getData(uint256(node)); - (, , , uint96 fuses, ) = _unpack(tokenData); + (, , , uint64 fuses, ) = _unpack(tokenData); return fuses; } @@ -141,7 +141,7 @@ contract FuseControllerUpgraded is address owner, address resolver, uint64 expiry, - uint96 fuses, + uint64 fuses, address renewalController ) = _unpack(tokenData); @@ -189,7 +189,7 @@ contract FuseControllerUpgraded is address owner, , uint64 expiry, - uint96 fuses, + uint64 fuses, address renewalController ) = _unpack(tokenData); bool isAuthorized = registry.getAuthorization(id, owner, msg.sender); @@ -210,7 +210,7 @@ contract FuseControllerUpgraded is address subnodeOwner, address subnodeResolver, uint64 subnodeExpiry, - uint96 subnodeFuses, + uint64 subnodeFuses, address subnodeRenewalController ) external { bytes memory tokenData = registry.getData(uint256(node)); @@ -246,7 +246,7 @@ contract FuseControllerUpgraded is // A function that sets the upgrade contract. function setUpgradeController( - IControllerUpgrade _upgradeContract + IControllerUpgradeTarget _upgradeContract ) external onlyOwner { upgradeContract = _upgradeContract; } @@ -269,7 +269,7 @@ contract FuseControllerUpgraded is address owner, address resolver, uint64 expiry, - uint96 fuses, + uint64 fuses, address renewalController ) { @@ -286,7 +286,7 @@ contract FuseControllerUpgraded is address owner, address resolver, uint64 expiry, - uint96 fuses, + uint64 fuses, address renewalController ) internal view returns (bytes memory /*tokenData*/) { return diff --git a/test/l2/TestL2Registry.js b/test/l2/TestL2Registry.js index 41a7d9572..792e2c101 100644 --- a/test/l2/TestL2Registry.js +++ b/test/l2/TestL2Registry.js @@ -62,7 +62,7 @@ contract.only('L2Registry', function (accounts) { assert.equal(await registry.balanceOf(deployerAddress, ROOT_NODE), 1) const packedData = ethers.utils.solidityPack( - ['address', 'address', 'address', 'uint64', 'uint32', 'address'], + ['address', 'address', 'address', 'uint64', 'uint64', 'address'], [ controller.address, ownerAddress, From 650072afade23dbf62c592f2b85406a25a743a7a Mon Sep 17 00:00:00 2001 From: nxt3d Date: Wed, 14 Feb 2024 09:21:32 -0500 Subject: [PATCH 09/15] make sure node is not expired --- contracts/l2/FuseController.sol | 4 ++++ contracts/l2/mocks/FuseControllerUpgraded.sol | 3 --- test/l2/TestL2Registry.js | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/contracts/l2/FuseController.sol b/contracts/l2/FuseController.sol index 1d248a1c9..0f968a4d5 100644 --- a/contracts/l2/FuseController.sol +++ b/contracts/l2/FuseController.sol @@ -80,6 +80,7 @@ contract FuseController is Ownable, IFuseController { require(value == 1); require(from == td.owner); require(operator == td.owner || operatorApproved); + require(!_isExpired(tokenData)); return _pack( @@ -98,6 +99,9 @@ contract FuseController is Ownable, IFuseController { uint256 /*id*/ ) external view returns (uint256) { (address owner, , , , ) = _unpack(tokenData); + if (_isExpired(tokenData)) { + return 0; + } return _owner == owner ? 1 : 0; } diff --git a/contracts/l2/mocks/FuseControllerUpgraded.sol b/contracts/l2/mocks/FuseControllerUpgraded.sol index 72c662712..43039823e 100644 --- a/contracts/l2/mocks/FuseControllerUpgraded.sol +++ b/contracts/l2/mocks/FuseControllerUpgraded.sol @@ -7,8 +7,6 @@ import "../IFuseController.sol"; import "../IControllerUpgradeTarget.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; -import "hardhat/console.sol"; - error Unauthorised(bytes32 node, address addr); error CannotUpgrade(); error nameExpired(bytes32 node); @@ -59,7 +57,6 @@ contract FuseControllerUpgraded is function ownerOf(bytes32 node) external view returns (address) { //get the tokenData bytes memory tokenData = registry.getData(uint256(node)); - console.logBytes(tokenData); (address owner, , , , ) = _unpack(tokenData); return owner; } diff --git a/test/l2/TestL2Registry.js b/test/l2/TestL2Registry.js index 792e2c101..299229845 100644 --- a/test/l2/TestL2Registry.js +++ b/test/l2/TestL2Registry.js @@ -112,7 +112,7 @@ contract.only('L2Registry', function (accounts) { labelhash('sub'), subnodeOwnerAddress, resolver.address, - 5184000, // 60 days + MAX_UINT64, 0, // no fuse EMPTY_ADDRESS, // no controller { from: ownerAddress }, @@ -121,7 +121,7 @@ contract.only('L2Registry', function (accounts) { assert.equal(await registry.balanceOf(subnodeOwnerAddress, TEST_SUBNODE), 1) assert.equal(await registry.resolver(TEST_SUBNODE), resolver.address) assert.equal(await controller.ownerOf(TEST_SUBNODE), subnodeOwnerAddress) - assert.equal(await controller.expiryOf(TEST_SUBNODE), 5184000) + assert.equal(await controller.expiryOf(TEST_SUBNODE), MAX_UINT64) assert.equal(await controller.fusesOf(TEST_SUBNODE), 0) assert.equal( await controller.renewalControllerOf(TEST_SUBNODE), From 30b473b07c94a2dd8da3831683e6b198cebe2790 Mon Sep 17 00:00:00 2001 From: nxt3d Date: Thu, 15 Feb 2024 10:37:17 -0500 Subject: [PATCH 10/15] add expiration checks --- contracts/l2/FuseController.sol | 68 +++++-- test/l2/TestL2Registry.js | 325 +++++++++++++++++++++----------- 2 files changed, 272 insertions(+), 121 deletions(-) diff --git a/contracts/l2/FuseController.sol b/contracts/l2/FuseController.sol index 0f968a4d5..601e9410e 100644 --- a/contracts/l2/FuseController.sol +++ b/contracts/l2/FuseController.sol @@ -46,14 +46,24 @@ contract FuseController is Ownable, IFuseController { function ownerOfWithData( bytes calldata tokenData ) external view returns (address) { - (address owner, , , , ) = _unpack(tokenData); + (bool isExpired, address owner, , , , ) = _isExpired(tokenData); + + if (isExpired) { + return address(0); + } return owner; } function ownerOf(bytes32 node) external view returns (address) { //get the tokenData bytes memory tokenData = registry.getData(uint256(node)); - (address owner, , , , ) = _unpack(tokenData); + + (bool isExpired, address owner, , , , ) = _isExpired(tokenData); + + if (isExpired) { + return address(0); + } + return owner; } @@ -80,7 +90,8 @@ contract FuseController is Ownable, IFuseController { require(value == 1); require(from == td.owner); require(operator == td.owner || operatorApproved); - require(!_isExpired(tokenData)); + (bool isExpired, , , , , ) = _isExpired(tokenData); + require(isExpired); return _pack( @@ -98,8 +109,8 @@ contract FuseController is Ownable, IFuseController { address _owner, uint256 /*id*/ ) external view returns (uint256) { - (address owner, , , , ) = _unpack(tokenData); - if (_isExpired(tokenData)) { + (bool isExpired, address owner, , , , ) = _isExpired(tokenData); + if (isExpired) { return 0; } return _owner == owner ? 1 : 0; @@ -108,7 +119,10 @@ contract FuseController is Ownable, IFuseController { function resolverFor( bytes calldata tokenData ) external view returns (address) { - (, address resolver, , , ) = _unpack(tokenData); + (bool isExpired, , address resolver, , , ) = _isExpired(tokenData); + if (isExpired) { + return address(0); + } return resolver; } @@ -120,16 +134,26 @@ contract FuseController is Ownable, IFuseController { } function fusesOf(bytes32 node) external view returns (uint64) { - // get the tokenData bytes memory tokenData = registry.getData(uint256(node)); - (, , , uint64 fuses, ) = _unpack(tokenData); + + (bool isExpired, , , , uint64 fuses, ) = _isExpired(tokenData); + + if (isExpired) { + return 0; + } return fuses; } function renewalControllerOf(bytes32 node) external view returns (address) { // get the tokenData bytes memory tokenData = registry.getData(uint256(node)); - (, , , , address renewalController) = _unpack(tokenData); + (bool isExpired, , , , , address renewalController) = _isExpired( + tokenData + ); + + if (isExpired) { + return address(0); + } return renewalController; } @@ -142,12 +166,13 @@ contract FuseController is Ownable, IFuseController { // Unpack the tokenData of the node. bytes memory tokenData = registry.getData(uint256(node)); ( + bool isExpired, address owner, address resolver, uint64 expiry, uint64 fuses, address renewalController - ) = _unpack(tokenData); + ) = _isExpired(tokenData); bool isAuthorized = registry.getAuthorization( uint256(node), @@ -159,7 +184,7 @@ contract FuseController is Ownable, IFuseController { revert Unauthorised(node, msg.sender); } - if (_isExpired(tokenData)) { + if (isExpired) { revert nameExpired(node); } @@ -265,9 +290,24 @@ contract FuseController is Ownable, IFuseController { * Internal functions * **********************/ - function _isExpired(bytes memory tokenData) internal view returns (bool) { - (, , uint64 expiry, , ) = _unpack(tokenData); - return expiry <= block.timestamp; + function _isExpired( + bytes memory tokenData + ) + internal + view + returns ( + bool isExpired, + address owner, + address resolver, + uint64 expiry, + uint64 fuses, + address renewalController + ) + { + (owner, resolver, expiry, fuses, renewalController) = _unpack( + tokenData + ); + isExpired = expiry <= block.timestamp; } function _unpack( diff --git a/test/l2/TestL2Registry.js b/test/l2/TestL2Registry.js index 299229845..f0b270c81 100644 --- a/test/l2/TestL2Registry.js +++ b/test/l2/TestL2Registry.js @@ -1,22 +1,68 @@ +const { ethers } = require('hardhat') +const { use, expect } = require('chai') +const { solidity } = require('ethereum-waffle') +const { labelhash, namehash, encodeName, FUSES } = require('../test-utils/ens') +const { evm } = require('../test-utils') const L2Registry = artifacts.require('L2Registry.sol') const RootController = artifacts.require('RootController.sol') const DelegatableResolver = artifacts.require('DelegatableResolver.sol') const FuseController = artifacts.require('FuseController.sol') const FuseControllerUpgraded = artifacts.require('FuseControllerUpgraded.sol') const StaticMetadataService = artifacts.require('StaticMetadataService.sol') -const { labelhash, namehash, encodeName, FUSES } = require('../test-utils/ens') -const ROOT_NODE = namehash('') const TEST_NODE = namehash('test') const TEST_SUBNODE = namehash('sub.test') const { deploy } = require('../test-utils/contracts') +//const { shouldBehaveLikeERC1155 } = require('./ERC1155.behaviour') +//const { shouldSupportInterfaces } = require('./SupportsInterface.behaviour') +//const { shouldRespectConstraints } = require('./Constraints.behaviour') +const { ZERO_ADDRESS } = require('@openzeppelin/test-helpers/src/constants') const { EMPTY_BYTES32, EMPTY_ADDRESS } = require('../test-utils/constants') -// The maximum value of a uint64 is 2^64 - 1 = 18446744073709551615 -// use BN instead of BigNumber to avoid BN error +const abiCoder = new ethers.utils.AbiCoder() + +use(solidity) + +const ROOT_NODE = EMPTY_BYTES32 + +const DUMMY_ADDRESS = '0x0000000000000000000000000000000000000001' +const DAY = 86400 +const GRACE_PERIOD = 90 * DAY -const MAX_UINT64 = '18446744073709551615' +function increaseTime(delay) { + return ethers.provider.send('evm_increaseTime', [delay]) +} -contract.only('L2Registry', function (accounts) { +function mine() { + return ethers.provider.send('evm_mine') +} + +const { + CANNOT_UNWRAP, + CANNOT_BURN_FUSES, + CANNOT_TRANSFER, + CANNOT_SET_RESOLVER, + CANNOT_SET_TTL, + CANNOT_CREATE_SUBDOMAIN, + PARENT_CANNOT_CONTROL, + CAN_DO_EVERYTHING, + IS_DOT_ETH, + CAN_EXTEND_EXPIRY, + CANNOT_APPROVE, +} = { + CAN_DO_EVERYTHING: 0, + CANNOT_UNWRAP: 1, + CANNOT_BURN_FUSES: 2, + CANNOT_TRANSFER: 4, + CANNOT_SET_RESOLVER: 8, + CANNOT_SET_TTL: 16, + CANNOT_CREATE_SUBDOMAIN: 32, + CANNOT_APPROVE: 64, + PARENT_CANNOT_CONTROL: 2 ** 16, + IS_DOT_ETH: 2 ** 17, + CAN_EXTEND_EXPIRY: 2 ** 18, +} + +describe.only('L2Registry', () => { let signers, deployer, deployerAddress, @@ -31,6 +77,7 @@ contract.only('L2Registry', function (accounts) { operator, delegate, metaDataservice + let MAX_EXPIRY = 2n ** 64n - 1n beforeEach(async () => { signers = await ethers.getSigners() @@ -67,7 +114,7 @@ contract.only('L2Registry', function (accounts) { controller.address, ownerAddress, resolver.address, - MAX_UINT64, + MAX_EXPIRY, 0, EMPTY_ADDRESS, ], @@ -84,134 +131,198 @@ contract.only('L2Registry', function (accounts) { assert.equal(await registry.balanceOf(ownerAddress, TEST_NODE), 1) assert.equal(await registry.resolver(TEST_NODE), resolver.address) assert.equal(await controller.ownerOf(TEST_NODE), ownerAddress) - assert.equal(await controller.expiryOf(TEST_NODE), MAX_UINT64) + assert.equal(await controller.expiryOf(TEST_NODE), MAX_EXPIRY) assert.equal(await controller.fusesOf(TEST_NODE), 0) assert.equal(await controller.renewalControllerOf(TEST_NODE), EMPTY_ADDRESS) }) - it('uri() returns url', async () => { - expect(await registry.uri(123)).to.equal('https://ens.domains') - }) - - it('owner can set a new MetadataService', async () => { - await registry.setMetadataService(dummyAccountAddress) - expect(await registry.metadataService()).to.equal(dummyAccountAddress) - }) - - it('non-owner cannot set a new MetadataService', async () => { - await expect( - registry.setMetadataService(dummyAccountAddress, { - from: hackerAddress, - }), - ).to.be.revertedWith('Ownable: caller is not the owner') + beforeEach(async () => { + result = await ethers.provider.send('evm_snapshot') }) - - it('should set a subnode on the test node', async () => { - await controller.setSubnode( - TEST_NODE, - labelhash('sub'), - subnodeOwnerAddress, - resolver.address, - MAX_UINT64, - 0, // no fuse - EMPTY_ADDRESS, // no controller - { from: ownerAddress }, - ) - assert.equal(await registry.controller(TEST_SUBNODE), controller.address) - assert.equal(await registry.balanceOf(subnodeOwnerAddress, TEST_SUBNODE), 1) - assert.equal(await registry.resolver(TEST_SUBNODE), resolver.address) - assert.equal(await controller.ownerOf(TEST_SUBNODE), subnodeOwnerAddress) - assert.equal(await controller.expiryOf(TEST_SUBNODE), MAX_UINT64) - assert.equal(await controller.fusesOf(TEST_SUBNODE), 0) - assert.equal( - await controller.renewalControllerOf(TEST_SUBNODE), - EMPTY_ADDRESS, - ) + afterEach(async () => { + await ethers.provider.send('evm_revert', [result]) }) - it('should set the resolver', async () => { - await controller.setResolver(TEST_NODE, dummyAddress, { - from: ownerAddress, + describe('??()', () => { + it('uri() returns url', async () => { + expect(await registry.uri(123)).to.equal('https://ens.domains') }) - assert.equal(await registry.resolver(TEST_NODE), dummyAddress) - }) - it('should set the resolver as an operator', async () => { - await registry.setApprovalForAll(operator.address, true, { - from: ownerAddress, + it('owner can set a new MetadataService', async () => { + await registry.setMetadataService(dummyAccountAddress) + expect(await registry.metadataService()).to.equal(dummyAccountAddress) }) - await controller.setResolver(TEST_NODE, dummyAddress, { - from: operator.address, + + it('non-owner cannot set a new MetadataService', async () => { + await expect( + registry.setMetadataService(dummyAccountAddress, { + from: hackerAddress, + }), + ).to.be.revertedWith('Ownable: caller is not the owner') }) - assert.equal(await registry.resolver(TEST_NODE), dummyAddress) - }) - it('should set the resolver as a delegate', async () => { - await registry.setApprovalForId(delegate.address, TEST_NODE, true, { - from: ownerAddress, + it('should set a subnode on the test node', async () => { + await controller.setSubnode( + TEST_NODE, + labelhash('sub'), + subnodeOwnerAddress, + resolver.address, + MAX_EXPIRY, + 0, // no fuse + EMPTY_ADDRESS, // no controller + { from: ownerAddress }, + ) + assert.equal(await registry.controller(TEST_SUBNODE), controller.address) + assert.equal( + await registry.balanceOf(subnodeOwnerAddress, TEST_SUBNODE), + 1, + ) + assert.equal(await registry.resolver(TEST_SUBNODE), resolver.address) + assert.equal(await controller.ownerOf(TEST_SUBNODE), subnodeOwnerAddress) + assert.equal(await controller.expiryOf(TEST_SUBNODE), MAX_EXPIRY) + assert.equal(await controller.fusesOf(TEST_SUBNODE), 0) + assert.equal( + await controller.renewalControllerOf(TEST_SUBNODE), + EMPTY_ADDRESS, + ) }) - await controller.setResolver(TEST_NODE, dummyAddress, { - from: delegate.address, + + it('should set the resolver', async () => { + await controller.setResolver(TEST_NODE, dummyAddress, { + from: ownerAddress, + }) + assert.equal(await registry.resolver(TEST_NODE), dummyAddress) }) - assert.equal(await registry.resolver(TEST_NODE), dummyAddress) - }) - it('should revert if the resolver is set as a delegate after the owner calls clearAllApprovedForIds', async () => { - await registry.setApprovalForId(delegate.address, TEST_NODE, true, { - from: ownerAddress, + it('should set the resolver as an operator', async () => { + await registry.setApprovalForAll(operator.address, true, { + from: ownerAddress, + }) + await controller.setResolver(TEST_NODE, dummyAddress, { + from: operator.address, + }) + assert.equal(await registry.resolver(TEST_NODE), dummyAddress) }) - await registry.clearAllApprovedForIds(ownerAddress, { from: ownerAddress }) - // make sure the set resolver fails expect revert without a reason - await expect( - controller.setResolver(TEST_NODE, dummyAddress, { - from: delegate.address, - }), - ).to.be.reverted - }) - // Check to make sure that a operator can call the setApprovalForId function. - it('should set the setApprovalForId as an operator', async () => { - await registry.setApprovalForAll(operator.address, true, { - from: ownerAddress, + it('should set the resolver as a delegate', async () => { + await registry.setApprovalForId(delegate.address, TEST_NODE, true, { + from: ownerAddress, + }) + await controller.setResolver(TEST_NODE, dummyAddress, { + from: delegate.address, + }) + assert.equal(await registry.resolver(TEST_NODE), dummyAddress) }) - await registry.setApprovalForId(dummyAddress, TEST_NODE, true, { - from: operator.address, + it('should revert if the resolver is set as a delegate after the owner calls clearAllApprovedForIds', async () => { + await registry.setApprovalForId(delegate.address, TEST_NODE, true, { + from: ownerAddress, + }) + await registry.clearAllApprovedForIds(ownerAddress, { + from: ownerAddress, + }) + // make sure the set resolver fails expect revert without a reason + await expect( + controller.setResolver(TEST_NODE, dummyAddress, { + from: delegate.address, + }), + ).to.be.reverted }) - assert.equal(await registry.isApprovedForId(TEST_NODE, dummyAddress), true) - }) + // Check to make sure that a operator can call the setApprovalForId function. + it('should set the setApprovalForId as an operator', async () => { + await registry.setApprovalForAll(operator.address, true, { + from: ownerAddress, + }) + + await registry.setApprovalForId(dummyAddress, TEST_NODE, true, { + from: operator.address, + }) - // Check to make sure we can upgrade the controller - it('should upgrade the controller', async () => { - // get the controller - const currentController = await registry.controller(TEST_NODE) - // set the upgraded controller on the controller. - await controller.setUpgradeController(controllerUpgraded.address, { - from: deployerAddress, + assert.equal( + await registry.isApprovedForId(TEST_NODE, dummyAddress), + true, + ) }) - // upgrade the controller of the TEST_NODE using the upgrade(node, extraData) function - await controller.upgrade(TEST_NODE, '0x', { - from: ownerAddress, + // Check to make sure we can upgrade the controller + it('should upgrade the controller', async () => { + // get the controller + const currentController = await registry.controller(TEST_NODE) + // set the upgraded controller on the controller. + await controller.setUpgradeController(controllerUpgraded.address, { + from: deployerAddress, + }) + + // upgrade the controller of the TEST_NODE using the upgrade(node, extraData) function + await controller.upgrade(TEST_NODE, '0x', { + from: ownerAddress, + }) + + // get the new controller + const _upgradedController = await registry.controller(TEST_NODE) + + // check to make sure the controller is the upgraded controller + assert.equal(_upgradedController, controllerUpgraded.address) + + // create am instace from the upgraded controller's address + _upgradedControllerInstance = await ethers.getContractAt( + 'FuseControllerUpgraded', + _upgradedController, + ) + + // check to make sure the owner is the same on the upgraded controller + assert.equal( + await _upgradedControllerInstance.ownerOf(TEST_NODE), + ownerAddress, + ) }) + it('should set a subnode, and then let the subnode expire', async () => { + // Get the block time in seconds + let blockTime = (await ethers.provider.getBlock('latest')).timestamp - // get the new controller - const _upgradedController = await registry.controller(TEST_NODE) + // An expiry that is 2 months in seconds beyond the current block time + let expiry = blockTime + 60 * DAY + + await controller.setSubnode( + TEST_NODE, + labelhash('sub'), + subnodeOwnerAddress, + resolver.address, + expiry, + 0, // no fuse + EMPTY_ADDRESS, // no controller + { from: ownerAddress }, + ) + assert.equal(await registry.controller(TEST_SUBNODE), controller.address) + assert.equal( + await registry.balanceOf(subnodeOwnerAddress, TEST_SUBNODE), + 1, + ) + assert.equal(await registry.resolver(TEST_SUBNODE), resolver.address) + assert.equal(await controller.ownerOf(TEST_SUBNODE), subnodeOwnerAddress) + assert.equal(await controller.expiryOf(TEST_SUBNODE), expiry) + assert.equal(await controller.fusesOf(TEST_SUBNODE), 0) + assert.equal( + await controller.renewalControllerOf(TEST_SUBNODE), + EMPTY_ADDRESS, + ) - // check to make sure the controller is the upgraded controller - assert.equal(_upgradedController, controllerUpgraded.address) + console.log('blockTime', blockTime) - // create am instace from the upgraded controller's address - _upgradedControllerInstance = await ethers.getContractAt( - 'FuseControllerUpgraded', - _upgradedController, - ) + await increaseTime(60 * DAY) + await mine() - // check to make sure the owner is the same on the upgraded controller - assert.equal( - await _upgradedControllerInstance.ownerOf(TEST_NODE), - ownerAddress, - ) + //Make sure all the values are set to the default values + assert.equal( + await registry.balanceOf(subnodeOwnerAddress, TEST_SUBNODE), + 0, + ) + // assert.equal(await registry.resolver(TEST_SUBNODE), EMPTY_ADDRESS) + // assert.equal(await controller.ownerOf(TEST_SUBNODE), EMPTY_ADDRESS) + // assert.equal(await controller.expiryOf(TEST_SUBNODE), expiry) + // assert.equal(await controller.fusesOf(TEST_SUBNODE), 0) + // assert.equal(await controller.renewalControllerOf(TEST_SUBNODE), EMPTY_ADDRESS) + }) }) }) From 245d7311d6cd2a7210817ae57939a9a65f7c0ad7 Mon Sep 17 00:00:00 2001 From: nxt3d Date: Tue, 20 Feb 2024 20:32:19 -0500 Subject: [PATCH 11/15] add fuse checks --- contracts/l2/FuseController.sol | 282 +++++++++++++++++++++++++++++-- contracts/l2/IFuseController.sol | 9 + test/l2/TestL2Registry.js | 112 +++++++++--- 3 files changed, 366 insertions(+), 37 deletions(-) diff --git a/contracts/l2/FuseController.sol b/contracts/l2/FuseController.sol index 601e9410e..29db31308 100644 --- a/contracts/l2/FuseController.sol +++ b/contracts/l2/FuseController.sol @@ -7,6 +7,8 @@ import "./IFuseController.sol"; import "./IControllerUpgradeTarget.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; +import "hardhat/console.sol"; + error Unauthorised(bytes32 node, address addr); error CannotUpgrade(); error nameExpired(bytes32 node); @@ -79,6 +81,11 @@ contract FuseController is Ownable, IFuseController { ) external view returns (bytes memory) { TokenData memory td; + // Make sure the tokenData is of the correct length. + if (tokenData.length < 96) { + revert("Invalid tokenData length"); + } + ( td.owner, td.resolver, @@ -87,11 +94,27 @@ contract FuseController is Ownable, IFuseController { td.renewalController ) = _unpack(tokenData); + require(msg.sender == address(registry), "Caller is not the registry"); require(value == 1); - require(from == td.owner); - require(operator == td.owner || operatorApproved); + require(from == td.owner, "From is not the owner"); + require( + operator == td.owner || operatorApproved, + "Operator not approved" + ); (bool isExpired, , , , , ) = _isExpired(tokenData); - require(isExpired); + require(!isExpired, "Token is expired"); + + // Make sure the CANNOT_TRANSFER fuse is not burned. + require((td.fuses & CANNOT_TRANSFER) == 0, "Cannot transfer"); + + // if the 'to' address is the zero address, then the token is being burned, and + // set all the values to the default values. + if (to == address(0)) { + // Make sure the CANNOT_BURN_NAME fuse is not burned. + require((td.fuses & CANNOT_BURN_NAME) == 0, "Cannot burn name"); + return + _pack(address(this), address(0), address(0), 0, 0, address(0)); + } return _pack( @@ -109,6 +132,11 @@ contract FuseController is Ownable, IFuseController { address _owner, uint256 /*id*/ ) external view returns (uint256) { + // if the tokenData is not of the correct length, return 0. + if (tokenData.length < 96) { + return 0; + } + (bool isExpired, address owner, , , , ) = _isExpired(tokenData); if (isExpired) { return 0; @@ -119,6 +147,11 @@ contract FuseController is Ownable, IFuseController { function resolverFor( bytes calldata tokenData ) external view returns (address) { + // if the tokenData is not of the correct length, return 0. + if (tokenData.length < 96) { + return address(0); + } + (bool isExpired, , address resolver, , , ) = _isExpired(tokenData); if (isExpired) { return address(0); @@ -129,13 +162,24 @@ contract FuseController is Ownable, IFuseController { function expiryOf(bytes32 node) external view returns (uint64) { // get the tokenData bytes memory tokenData = registry.getData(uint256(node)); + + // if the tokenData is not of the correct length, return 0. + if (tokenData.length < 96) { + return 0; + } + (, , uint64 expiry, , ) = _unpack(tokenData); return expiry; } - function fusesOf(bytes32 node) external view returns (uint64) { + function fusesOf(bytes32 node) public view returns (uint64) { bytes memory tokenData = registry.getData(uint256(node)); + // if the tokenData is not of the correct length, return 0. + if (tokenData.length < 96) { + return 0; + } + (bool isExpired, , , , uint64 fuses, ) = _isExpired(tokenData); if (isExpired) { @@ -147,6 +191,12 @@ contract FuseController is Ownable, IFuseController { function renewalControllerOf(bytes32 node) external view returns (address) { // get the tokenData bytes memory tokenData = registry.getData(uint256(node)); + + // if the tokenData is not of the correct length, return 0. + if (tokenData.length < 96) { + return address(0); + } + (bool isExpired, , , , , address renewalController) = _isExpired( tokenData ); @@ -209,7 +259,53 @@ contract FuseController is Ownable, IFuseController { * Node Owner functions * *******************/ + // A setFuses function that allows the owner of a node to set the fuses of the node. + function setFuses(uint256 id, uint64 fuses) external { + // get tokenData + bytes memory tokenData = registry.getData(id); + ( + address owner, + address resolver, + uint64 expiry, + uint64 oldFuses, + address renewalController + ) = _unpack(tokenData); + + bool isAuthorized = registry.getAuthorization(id, owner, msg.sender); + + if (owner != msg.sender && !isAuthorized) { + revert Unauthorised(bytes32(id), msg.sender); + } + + // Make sure that the CANNOT_BURN_FUSES is not burned. + require((oldFuses & CANNOT_BURN_FUSES) == 0, "Cannot burn fuses"); + + // Make sure that PARENT_CANNOT_CONTROL is burned. + require( + (oldFuses & PARENT_CANNOT_CONTROL) != 0, + "Parent cannot control" + ); + + registry.setNode( + id, + _pack( + address(this), + owner, + resolver, + expiry, + fuses, + renewalController + ) + ); + } + function setResolver(uint256 id, address newResolver) external { + // Check to make sure that the fuse CANNOT_SET_RESOLVER is not burned. + require( + (fusesOf(bytes32(id)) & CANNOT_SET_RESOLVER) == 0, + "Cannot set resolver" + ); + // get tokenData bytes memory tokenData = registry.getData(id); ( @@ -238,6 +334,117 @@ contract FuseController is Ownable, IFuseController { ); } + // Set the expiry of a subnode, with a node and a label. + function setExpiry(bytes32 node, uint256 label, uint64 newExpiry) external { + // get the subnode + bytes32 subnode = keccak256(abi.encodePacked(node, label)); + + // get tokenData + bytes memory tokenData = registry.getData(uint256(subnode)); + + // Make sure the parent node controller is this contract. + require( + address(_getController(tokenData)) == address(this), + "Controller is not this contract" + ); + + ( + address owner, + address resolver, // we don't need the old expiry + , + /*uint64 expiry*/ uint64 fuses, + address renewalController + ) = _unpack(tokenData); + + // Check to make sure partent cannot control is not burned. + require((fuses & PARENT_CANNOT_CONTROL) != 0, "Parent cannot control"); + + // Make sure the caller is authroized in the parent node. + bool isAuthorized = registry.getAuthorization( + uint256(node), + owner, + msg.sender + ); + + if (owner != msg.sender && !isAuthorized) { + revert Unauthorised(node, msg.sender); + } + + registry.setNode( + uint256(subnode), + _pack( + address(this), + owner, + resolver, + newExpiry, + fuses, + renewalController + ) + ); + } + + // Set node function that allows the owner of a node to set the node. + function setNode( + uint256 id, + address owner, + address resolver, + uint64 fuses, + address renewalController + ) external { + TokenData memory tdOld; + + // get tokenData + bytes memory tokenData = registry.getData(id); + (tdOld.owner, tdOld.resolver, tdOld.expiry, tdOld.fuses, ) = _unpack( + tokenData + ); + + bool isAuthorized = registry.getAuthorization( + id, + tdOld.owner, + msg.sender + ); + + if (tdOld.owner != msg.sender && !isAuthorized) { + revert Unauthorised(bytes32(id), msg.sender); + } + + // If fuses are being burned. + if (fuses != 0) { + // Make sure that the CANNOT_BURN_NAME is not burned. + require((tdOld.fuses & CANNOT_BURN_FUSES) == 0, "Cannot burn name"); + + // Make sure that PARENT_CANNOT_CONTROL is burned. + require( + (tdOld.fuses & PARENT_CANNOT_CONTROL) != 0, + "Parent cannot control" + ); + } + + // If the resolver is being changed. + if (resolver != tdOld.resolver) { + // Make sure that the CANNOT_SET_RESOLVER is not burned. + require( + (tdOld.fuses & CANNOT_SET_RESOLVER) == 0, + "Cannot set resolver" + ); + } + + // If the resolver is being set. + + registry.setNode( + id, + _pack( + address(this), + owner, + resolver, + tdOld.expiry, + fuses | tdOld.fuses, + renewalController + ) + ); + } + function setSubnode( bytes32 node, uint256 label, @@ -247,15 +454,59 @@ contract FuseController is Ownable, IFuseController { uint64 subnodeFuses, address subnodeRenewalController ) external { + TokenData memory tdNode; + bytes memory tokenData = registry.getData(uint256(node)); - (address owner, , , , ) = _unpack(tokenData); + + // Make sure the parent node controller is this contract. + require( + address(_getController(tokenData)) == address(this), + "Controller is not this contract" + ); + + (tdNode.owner, , , tdNode.fuses, ) = _unpack(tokenData); + + // Check to make sure that the fuse CANNOT_CREATE_SUBDOMAIN is not burned. + require( + (tdNode.fuses & CANNOT_CREATE_SUBDOMAIN) == 0, + "Cannot create subdomain" + ); + + // Make the node of the subnode. + bytes32 subnode = keccak256(abi.encodePacked(node, label)); + + // Get the subnode fuses. + uint64 subnodeFusesOld = fusesOf(subnode); + + // If subnode fuses are being burned. + if (subnodeFuses != 0) { + require( + ((tdNode.fuses & CANNOT_BURN_NAME) | PARENT_CANNOT_CONTROL) == + CANNOT_BURN_NAME | PARENT_CANNOT_CONTROL, + "The parent node is missing required fuses" + ); + + // Make sure that the CANNOT_BURN_FUSES is not burned in the existing subnode. + require( + (subnodeFusesOld & CANNOT_BURN_FUSES) == 0, + "Cannot burn fuses" + ); + + // Make sure that PARENT_CANNOT_CONTROL is burned already on the subnode, + // or is being burned. + require( + ((subnodeFusesOld | subnodeFuses) & PARENT_CANNOT_CONTROL) != 0, + "Parent cannot control" + ); + } + bool isAuthorized = registry.getAuthorization( uint256(node), - owner, + tdNode.owner, msg.sender ); - if (owner != msg.sender && !isAuthorized) { + if (tdNode.owner != msg.sender && !isAuthorized) { revert Unauthorised(node, msg.sender); } @@ -267,7 +518,7 @@ contract FuseController is Ownable, IFuseController { subnodeOwner, subnodeResolver, subnodeExpiry, - subnodeFuses, + subnodeFusesOld | subnodeFuses, // if there were fuses, then add them to the existing fuses. subnodeRenewalController ), msg.sender, @@ -314,7 +565,7 @@ contract FuseController is Ownable, IFuseController { bytes memory tokenData ) internal - view + pure returns ( address owner, address resolver, @@ -341,7 +592,7 @@ contract FuseController is Ownable, IFuseController { uint64 expiry, uint64 fuses, address renewalController - ) internal view returns (bytes memory /*tokenData*/) { + ) internal pure returns (bytes memory /*tokenData*/) { return abi.encodePacked( controller, @@ -352,4 +603,15 @@ contract FuseController is Ownable, IFuseController { renewalController ); } + + function _getController( + bytes memory data + ) internal pure returns (IController addr) { + if (data.length < 20) { + return IController(address(0)); + } + assembly { + addr := mload(add(data, 20)) + } + } } diff --git a/contracts/l2/IFuseController.sol b/contracts/l2/IFuseController.sol index ec5a11e58..4319b11b4 100644 --- a/contracts/l2/IFuseController.sol +++ b/contracts/l2/IFuseController.sol @@ -4,6 +4,15 @@ pragma solidity ^0.8.17; import "./IController.sol"; import "./IControllerUpgradeTarget.sol"; +uint64 constant CAN_DO_EVERYTHING = 0; +uint64 constant CANNOT_BURN_NAME = 1; +uint64 constant CANNOT_BURN_FUSES = 2; +uint64 constant CANNOT_TRANSFER = 4; +uint64 constant CANNOT_SET_RESOLVER = 8; +uint64 constant CANNOT_CREATE_SUBDOMAIN = 16; +uint64 constant CANNOT_SET_RENEWAL_CONTROLLER = 32; +uint64 constant PARENT_CANNOT_CONTROL = 64; + interface IFuseController is IController { function expiryOf(bytes32 node) external view returns (uint64); diff --git a/test/l2/TestL2Registry.js b/test/l2/TestL2Registry.js index f0b270c81..ec6374992 100644 --- a/test/l2/TestL2Registry.js +++ b/test/l2/TestL2Registry.js @@ -37,29 +37,23 @@ function mine() { } const { - CANNOT_UNWRAP, + CAN_DO_EVERYTHING, + CANNOT_BURN_NAME, CANNOT_BURN_FUSES, CANNOT_TRANSFER, CANNOT_SET_RESOLVER, - CANNOT_SET_TTL, CANNOT_CREATE_SUBDOMAIN, + CANNOT_SET_RENEWAL_CONTROLLER, PARENT_CANNOT_CONTROL, - CAN_DO_EVERYTHING, - IS_DOT_ETH, - CAN_EXTEND_EXPIRY, - CANNOT_APPROVE, } = { CAN_DO_EVERYTHING: 0, - CANNOT_UNWRAP: 1, - CANNOT_BURN_FUSES: 2, - CANNOT_TRANSFER: 4, - CANNOT_SET_RESOLVER: 8, - CANNOT_SET_TTL: 16, - CANNOT_CREATE_SUBDOMAIN: 32, - CANNOT_APPROVE: 64, - PARENT_CANNOT_CONTROL: 2 ** 16, - IS_DOT_ETH: 2 ** 17, - CAN_EXTEND_EXPIRY: 2 ** 18, + CANNOT_BURN_NAME: 1, + CANNOT_BURN_FUSES: 2 ** 1, + CANNOT_TRANSFER: 2 ** 2, + CANNOT_SET_RESOLVER: 2 ** 3, + CANNOT_CREATE_SUBDOMAIN: 2 ** 4, + CANNOT_SET_RENEWAL_CONTROLLER: 2 ** 5, + PARENT_CANNOT_CONTROL: 2 ** 6, } describe.only('L2Registry', () => { @@ -108,14 +102,14 @@ describe.only('L2Registry', () => { // test to make sure the root node is owned by the deployer assert.equal(await registry.balanceOf(deployerAddress, ROOT_NODE), 1) - const packedData = ethers.utils.solidityPack( + const testNodeData = ethers.utils.solidityPack( ['address', 'address', 'address', 'uint64', 'uint64', 'address'], [ controller.address, ownerAddress, resolver.address, MAX_EXPIRY, - 0, + CANNOT_BURN_NAME | PARENT_CANNOT_CONTROL, EMPTY_ADDRESS, ], ) @@ -124,7 +118,7 @@ describe.only('L2Registry', () => { registry.address, 0, // This is ignored because the ROOT_NODE is fixed in the root controller. labelhash('test'), - packedData, + testNodeData, ) assert.equal(await registry.controller(TEST_NODE), controller.address) @@ -132,7 +126,10 @@ describe.only('L2Registry', () => { assert.equal(await registry.resolver(TEST_NODE), resolver.address) assert.equal(await controller.ownerOf(TEST_NODE), ownerAddress) assert.equal(await controller.expiryOf(TEST_NODE), MAX_EXPIRY) - assert.equal(await controller.fusesOf(TEST_NODE), 0) + assert.equal( + await controller.fusesOf(TEST_NODE), + CANNOT_BURN_NAME | PARENT_CANNOT_CONTROL, + ) assert.equal(await controller.renewalControllerOf(TEST_NODE), EMPTY_ADDRESS) }) @@ -168,7 +165,7 @@ describe.only('L2Registry', () => { subnodeOwnerAddress, resolver.address, MAX_EXPIRY, - 0, // no fuse + CANNOT_SET_RESOLVER | CANNOT_BURN_NAME | PARENT_CANNOT_CONTROL, // no fuse EMPTY_ADDRESS, // no controller { from: ownerAddress }, ) @@ -180,7 +177,10 @@ describe.only('L2Registry', () => { assert.equal(await registry.resolver(TEST_SUBNODE), resolver.address) assert.equal(await controller.ownerOf(TEST_SUBNODE), subnodeOwnerAddress) assert.equal(await controller.expiryOf(TEST_SUBNODE), MAX_EXPIRY) - assert.equal(await controller.fusesOf(TEST_SUBNODE), 0) + assert.equal( + await controller.fusesOf(TEST_SUBNODE), + CANNOT_SET_RESOLVER | CANNOT_BURN_NAME | PARENT_CANNOT_CONTROL, + ) assert.equal( await controller.renewalControllerOf(TEST_SUBNODE), EMPTY_ADDRESS, @@ -318,11 +318,69 @@ describe.only('L2Registry', () => { await registry.balanceOf(subnodeOwnerAddress, TEST_SUBNODE), 0, ) - // assert.equal(await registry.resolver(TEST_SUBNODE), EMPTY_ADDRESS) - // assert.equal(await controller.ownerOf(TEST_SUBNODE), EMPTY_ADDRESS) - // assert.equal(await controller.expiryOf(TEST_SUBNODE), expiry) - // assert.equal(await controller.fusesOf(TEST_SUBNODE), 0) - // assert.equal(await controller.renewalControllerOf(TEST_SUBNODE), EMPTY_ADDRESS) + assert.equal(await registry.resolver(TEST_SUBNODE), EMPTY_ADDRESS) + assert.equal(await controller.ownerOf(TEST_SUBNODE), EMPTY_ADDRESS) + assert.equal(await controller.expiryOf(TEST_SUBNODE), expiry) + assert.equal(await controller.fusesOf(TEST_SUBNODE), 0) + assert.equal( + await controller.renewalControllerOf(TEST_SUBNODE), + EMPTY_ADDRESS, + ) + }) + + // make sure that the name can't be transferred when the CANNOT_TRANSFER fuse is set + it('should set the CANNOT_TRANSFER fuse', async () => { + await controller.setFuses(TEST_NODE, CANNOT_TRANSFER, { + from: ownerAddress, + }) + assert.equal(await controller.fusesOf(TEST_NODE), CANNOT_TRANSFER) + await expect( + registry.safeTransferFrom( + ownerAddress, + dummyAddress, + TEST_NODE, + 1, + '0x', + { + from: ownerAddress, + }, + ), + ).to.be.revertedWith('') + }) + + // Make sure the resolver can't be set when the CANNOT_SET_RESOLVER fuse is burned + it('should set the CANNOT_SET_RESOLVER fuse', async () => { + await controller.setFuses(TEST_NODE, CANNOT_SET_RESOLVER, { + from: ownerAddress, + }) + assert.equal(await controller.fusesOf(TEST_NODE), CANNOT_SET_RESOLVER) + await expect( + controller.setResolver(TEST_NODE, dummyAddress, { + from: ownerAddress, + }), + ).to.be.revertedWith('') + }) + + it('should transfer the name to the zero address', async () => { + await registry.safeTransferFrom( + ownerAddress, + ZERO_ADDRESS, + TEST_NODE, + 1, + '0x', + { + from: ownerAddress, + }, + ) + assert.equal(await registry.balanceOf(ownerAddress, TEST_NODE), 0) + assert.equal(await registry.resolver(TEST_NODE), EMPTY_ADDRESS) + assert.equal(await controller.ownerOf(TEST_NODE), EMPTY_ADDRESS) + assert.equal(await controller.expiryOf(TEST_NODE), 0) + assert.equal(await controller.fusesOf(TEST_NODE), 0) + assert.equal( + await controller.renewalControllerOf(TEST_NODE), + EMPTY_ADDRESS, + ) }) }) }) From eaa6a02d99aec92b79500128314ef4e70381de23 Mon Sep 17 00:00:00 2001 From: nxt3d Date: Wed, 21 Feb 2024 10:09:02 -0500 Subject: [PATCH 12/15] add burn functions --- contracts/l2/FuseController.sol | 52 +++++++++++++++---- contracts/l2/IController.sol | 10 ++++ contracts/l2/L2Registry.sol | 49 +++++++++++++++++ contracts/l2/RootController.sol | 13 +++++ contracts/l2/mocks/FuseControllerUpgraded.sol | 10 ++++ test/l2/TestL2Registry.js | 50 +++++++++++++----- 6 files changed, 161 insertions(+), 23 deletions(-) diff --git a/contracts/l2/FuseController.sol b/contracts/l2/FuseController.sol index 29db31308..774f9867b 100644 --- a/contracts/l2/FuseController.sol +++ b/contracts/l2/FuseController.sol @@ -107,15 +107,6 @@ contract FuseController is Ownable, IFuseController { // Make sure the CANNOT_TRANSFER fuse is not burned. require((td.fuses & CANNOT_TRANSFER) == 0, "Cannot transfer"); - // if the 'to' address is the zero address, then the token is being burned, and - // set all the values to the default values. - if (to == address(0)) { - // Make sure the CANNOT_BURN_NAME fuse is not burned. - require((td.fuses & CANNOT_BURN_NAME) == 0, "Cannot burn name"); - return - _pack(address(this), address(0), address(0), 0, 0, address(0)); - } - return _pack( address(this), @@ -127,6 +118,49 @@ contract FuseController is Ownable, IFuseController { ); } + function burn( + bytes calldata tokenData, + address operator, + address from, + uint256 /*id*/, + uint256 value, + bytes calldata /*data*/, + bool operatorApproved + ) external view returns (bytes memory) { + TokenData memory td; + + // Make sure the tokenData is of the correct length. + if (tokenData.length < 96) { + revert("Invalid tokenData length"); + } + + ( + td.owner, + td.resolver, + td.expiry, + td.fuses, + td.renewalController + ) = _unpack(tokenData); + + require(msg.sender == address(registry), "Caller is not the registry"); + require(value == 1); + require(from == td.owner, "From is not the owner"); + require( + operator == td.owner || operatorApproved, + "Operator not approved" + ); + (bool isExpired, , , , , ) = _isExpired(tokenData); + require(!isExpired, "Token is expired"); + + // Make sure the CANNOT_BURN_NAME and CANNOT_TRANSFER fuse is not burned. + require( + (td.fuses & (CANNOT_BURN_NAME | CANNOT_TRANSFER)) == 0, + "Cannot burn or transfer" + ); + + return _pack(address(this), address(0), address(0), 0, 0, address(0)); + } + function balanceOf( bytes calldata tokenData, address _owner, diff --git a/contracts/l2/IController.sol b/contracts/l2/IController.sol index b1c55fe95..07f0eac1c 100644 --- a/contracts/l2/IController.sol +++ b/contracts/l2/IController.sol @@ -20,6 +20,16 @@ interface IController { bool isApproved ) external returns (bytes memory); + function burn( + bytes calldata tokenData, + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data, + bool operatorApproved + ) external view returns (bytes memory); + function balanceOf( bytes calldata tokenData, address owner, diff --git a/contracts/l2/L2Registry.sol b/contracts/l2/L2Registry.sol index 4cfa348e8..9a15d3bf3 100644 --- a/contracts/l2/L2Registry.sol +++ b/contracts/l2/L2Registry.sol @@ -93,6 +93,26 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { ); } + function burn(address from, uint256 id, uint256 /*value*/) external { + _burn(from, id); + emit TransferSingle(msg.sender, from, address(0), id, 1); + } + + function burnBatch( + address from, + uint256[] memory ids, + uint256[] calldata /*values*/ + ) external { + // make an empty uint256 array for the value of 1 for each id + uint256[] memory onesArray = new uint256[](ids.length); + for (uint256 i = 0; i < ids.length; i++) { + _burn(from, ids[i]); + // fill the ones array with 1s + onesArray[i] = 1; + } + emit TransferBatch(msg.sender, from, address(0), ids, onesArray); + } + function setApprovalForAll(address operator, bool approved) external { approvals[msg.sender][operator] = approved; emit ApprovalForAll(msg.sender, operator, approved); @@ -309,6 +329,13 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { uint256 value, bytes calldata data ) internal { + if (to == address(0)) { + revert("Cannot transfer to the zero address"); + } + if (from == address(0)) { + revert("Cannot transfer from the zero address"); + } + bytes memory tokenData = tokens[id].data; IController oldController = _getController(tokenData); if (address(oldController) == address(0)) { @@ -331,6 +358,28 @@ contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { tokens[id].data = newTokenData; } + function _burn(address from, uint256 id) internal { + bytes memory tokenData = tokens[id].data; + IController oldController = _getController(tokenData); + if (address(oldController) == address(0)) { + revert TokenDoesNotExist(id); + } + bool isApproved = approvals[from][msg.sender] || + tokenApprovals[from][tokenApprovalsNonce[from]][id][msg.sender]; + + bytes memory newTokenData = oldController.burn( + tokenData, + msg.sender, + from, + id, + 1, + bytes(""), + isApproved + ); + + tokens[id].data = newTokenData; + } + function _doSafeTransferAcceptanceCheck( address operator, address from, diff --git a/contracts/l2/RootController.sol b/contracts/l2/RootController.sol index 2f47c16c3..2d07801a9 100644 --- a/contracts/l2/RootController.sol +++ b/contracts/l2/RootController.sol @@ -18,6 +18,7 @@ contract RootController is Ownable, IController { } error CannotTransfer(); + error CannotBurn(); event NewResolver(uint256 id, address resolver); @@ -48,6 +49,18 @@ contract RootController is Ownable, IController { revert CannotTransfer(); } + function burn( + bytes calldata /*tokenData*/, + address /*operator*/, + address /*from*/, + uint256 /*id*/, + uint256 /*value*/, + bytes calldata /*data*/, + bool /*operatorApproved*/ + ) external view returns (bytes memory) { + revert CannotBurn(); + } + function balanceOf( bytes calldata /*tokenData*/, address _owner, diff --git a/contracts/l2/mocks/FuseControllerUpgraded.sol b/contracts/l2/mocks/FuseControllerUpgraded.sol index 43039823e..1a50f75cf 100644 --- a/contracts/l2/mocks/FuseControllerUpgraded.sol +++ b/contracts/l2/mocks/FuseControllerUpgraded.sol @@ -89,6 +89,16 @@ contract FuseControllerUpgraded is _pack(to, td.resolver, td.expiry, td.fuses, td.renewalController); } + function burn( + bytes calldata /*tokenData*/, + address /*operator*/, + address /*from*/, + uint256 /*id*/, + uint256 /*value*/, + bytes calldata /*data*/, + bool /*operatorApproved*/ + ) external view returns (bytes memory) {} + function balanceOf( bytes calldata tokenData, address _owner, diff --git a/test/l2/TestL2Registry.js b/test/l2/TestL2Registry.js index ec6374992..7507bfc5c 100644 --- a/test/l2/TestL2Registry.js +++ b/test/l2/TestL2Registry.js @@ -361,24 +361,46 @@ describe.only('L2Registry', () => { ).to.be.revertedWith('') }) - it('should transfer the name to the zero address', async () => { - await registry.safeTransferFrom( - ownerAddress, - ZERO_ADDRESS, + // Make sure that a subname called 'sub' can be created and then burned by the owner + it('should create and burn a subnode', async () => { + await controller.setSubnode( TEST_NODE, + labelhash('sub'), + subnodeOwnerAddress, + resolver.address, + MAX_EXPIRY, + 0, // no fuse + EMPTY_ADDRESS, // no controller + { from: ownerAddress }, + ) + assert.equal(await registry.controller(TEST_SUBNODE), controller.address) + assert.equal( + await registry.balanceOf(subnodeOwnerAddress, TEST_SUBNODE), 1, - '0x', - { - from: ownerAddress, - }, ) - assert.equal(await registry.balanceOf(ownerAddress, TEST_NODE), 0) - assert.equal(await registry.resolver(TEST_NODE), EMPTY_ADDRESS) - assert.equal(await controller.ownerOf(TEST_NODE), EMPTY_ADDRESS) - assert.equal(await controller.expiryOf(TEST_NODE), 0) - assert.equal(await controller.fusesOf(TEST_NODE), 0) + assert.equal(await registry.resolver(TEST_SUBNODE), resolver.address) + assert.equal(await controller.ownerOf(TEST_SUBNODE), subnodeOwnerAddress) + assert.equal(await controller.expiryOf(TEST_SUBNODE), MAX_EXPIRY) + assert.equal(await controller.fusesOf(TEST_SUBNODE), 0) + assert.equal( + await controller.renewalControllerOf(TEST_SUBNODE), + EMPTY_ADDRESS, + ) + + await registry.burn(subnodeOwnerAddress, TEST_SUBNODE, 1, { + from: subnodeOwnerAddress, + }) + assert.equal( - await controller.renewalControllerOf(TEST_NODE), + await registry.balanceOf(subnodeOwnerAddress, TEST_SUBNODE), + 0, + ) + assert.equal(await registry.resolver(TEST_SUBNODE), EMPTY_ADDRESS) + assert.equal(await controller.ownerOf(TEST_SUBNODE), EMPTY_ADDRESS) + assert.equal(await controller.expiryOf(TEST_SUBNODE), 0) + assert.equal(await controller.fusesOf(TEST_SUBNODE), 0) + assert.equal( + await controller.renewalControllerOf(TEST_SUBNODE), EMPTY_ADDRESS, ) }) From aa23c1f98133258ec8d82096fef805ff63ff7feb Mon Sep 17 00:00:00 2001 From: nxt3d Date: Thu, 22 Feb 2024 22:03:45 -0500 Subject: [PATCH 13/15] allow for renewal controllers to renew a name --- contracts/l2/FuseController.sol | 76 ++++++++++++----- test/l2/TestL2Registry.js | 141 ++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 21 deletions(-) diff --git a/contracts/l2/FuseController.sol b/contracts/l2/FuseController.sol index 774f9867b..cd1e49a0a 100644 --- a/contracts/l2/FuseController.sol +++ b/contracts/l2/FuseController.sol @@ -198,7 +198,7 @@ contract FuseController is Ownable, IFuseController { bytes memory tokenData = registry.getData(uint256(node)); // if the tokenData is not of the correct length, return 0. - if (tokenData.length < 96) { + if (tokenData.length != 96) { return 0; } @@ -369,12 +369,19 @@ contract FuseController is Ownable, IFuseController { } // Set the expiry of a subnode, with a node and a label. - function setExpiry(bytes32 node, uint256 label, uint64 newExpiry) external { + function setExpiry( + bytes32 node, + bytes32 labelhash, + uint64 newExpiry + ) external { + TokenData memory td; + TokenData memory sub; + // get the subnode - bytes32 subnode = keccak256(abi.encodePacked(node, label)); + bytes32 subnode = keccak256(abi.encodePacked(node, labelhash)); // get tokenData - bytes memory tokenData = registry.getData(uint256(subnode)); + bytes memory tokenData = registry.getData(uint256(node)); // Make sure the parent node controller is this contract. require( @@ -382,37 +389,64 @@ contract FuseController is Ownable, IFuseController { "Controller is not this contract" ); + // Make sure the tokenData is 96 bytes long. + require(tokenData.length == 96, "Invalid tokenData length"); + ( - address owner, - address resolver, // we don't need the old expiry + td.owner, // resolver // expiry , - /*uint64 expiry*/ uint64 fuses, - address renewalController + , + td.fuses, + td.renewalController ) = _unpack(tokenData); - // Check to make sure partent cannot control is not burned. - require((fuses & PARENT_CANNOT_CONTROL) != 0, "Parent cannot control"); - // Make sure the caller is authroized in the parent node. bool isAuthorized = registry.getAuthorization( uint256(node), - owner, + td.owner, msg.sender ); - if (owner != msg.sender && !isAuthorized) { - revert Unauthorised(node, msg.sender); + // get tokenDataSubnode + bytes memory tokenDataSubnode = registry.getData(uint256(subnode)); + + // Get the data of the subnode, including the fuses and renewal controller, get the data + (sub.owner, sub.resolver, , sub.fuses, sub.renewalController) = _unpack( + tokenDataSubnode + ); + + // Check to make sure partent cannot control is not burned. + require( + (sub.fuses & PARENT_CANNOT_CONTROL) == 0, + "Parent cannot control" + ); + + // Check to make sure the caller is authorized. + if ( + // Allow the owner of the parent node to set the expiry. + !(td.owner == msg.sender) && + // Allow an authorized user of the parent node to set the expiry. + !(isAuthorized) && + // Allow the renewal controller of the parent node + // as long as the there is no renewal controller set on the subnode + // to set the expiry. + !(td.renewalController == msg.sender && + sub.renewalController == address(0)) && + // Allow the renewal controller of the subnode to set the expiry. + !(sub.renewalController == msg.sender) + ) { + revert Unauthorised(subnode, msg.sender); } registry.setNode( uint256(subnode), _pack( address(this), - owner, - resolver, + sub.owner, + sub.resolver, newExpiry, - fuses, - renewalController + sub.fuses, + sub.renewalController ) ); } @@ -481,7 +515,7 @@ contract FuseController is Ownable, IFuseController { function setSubnode( bytes32 node, - uint256 label, + bytes32 labelhash, address subnodeOwner, address subnodeResolver, uint64 subnodeExpiry, @@ -507,7 +541,7 @@ contract FuseController is Ownable, IFuseController { ); // Make the node of the subnode. - bytes32 subnode = keccak256(abi.encodePacked(node, label)); + bytes32 subnode = keccak256(abi.encodePacked(node, labelhash)); // Get the subnode fuses. uint64 subnodeFusesOld = fusesOf(subnode); @@ -546,7 +580,7 @@ contract FuseController is Ownable, IFuseController { registry.setSubnode( uint256(node), - label, + uint256(labelhash), _pack( address(this), subnodeOwner, diff --git a/test/l2/TestL2Registry.js b/test/l2/TestL2Registry.js index 7507bfc5c..798d60645 100644 --- a/test/l2/TestL2Registry.js +++ b/test/l2/TestL2Registry.js @@ -85,6 +85,8 @@ describe.only('L2Registry', () => { hackerAddress = await hacker.getAddress() dummyAccount = await signers[4] dummyAccountAddress = await dummyAccount.getAddress() + renewalController = await signers[5] + renewalControllerAddress = await renewalController.getAddress() resolver = await DelegatableResolver.new() metaDataservice = await StaticMetadataService.new('https://ens.domains') @@ -404,5 +406,144 @@ describe.only('L2Registry', () => { EMPTY_ADDRESS, ) }) + + // Make sure that a test subnode can be renewed by the renewal controller address. + it('should renew a subnode using the renewal controller', async () => { + const blockTime = (await ethers.provider.getBlock('latest')).timestamp + + await controller.setSubnode( + TEST_NODE, + labelhash('sub'), + subnodeOwnerAddress, + resolver.address, + // blocktime + 60 DAYs + blockTime + 60 * DAY, + 0, // no fuse + renewalControllerAddress, // no controller + { from: ownerAddress }, + ) + + // Make sure the subnode is owned by the subnodeOwnerAddress + assert.equal(await controller.ownerOf(TEST_SUBNODE), subnodeOwnerAddress) + + // Make sure the renewal controller is set to the renewalControllerAddress + assert.equal( + await controller.renewalControllerOf(TEST_SUBNODE), + renewalControllerAddress, + ) + + // Extend the expiry of the subnode by 30 days by calling the setExpiry function from the renewal controller address. + await controller.setExpiry( + TEST_NODE, + labelhash('sub'), + blockTime + 90 * DAY, + { + from: renewalControllerAddress, + }, + ) + + // Make sure the subnode is still owned by the subnodeOwnerAddress + assert.equal(await controller.ownerOf(TEST_SUBNODE), subnodeOwnerAddress) + + // Make sure the expiry of the subnode has been extended by 30 days + assert.equal( + await controller.expiryOf(TEST_SUBNODE), + blockTime + 90 * DAY, + ) + }) + + // Make sure that the test subnode can be renewed by the parent renewal controller address. + it('should renew a subnode using the parent renewal controller', async () => { + const blockTime = (await ethers.provider.getBlock('latest')).timestamp + + await controller.setSubnode( + TEST_NODE, + labelhash('sub'), + subnodeOwnerAddress, + resolver.address, + // blocktime + 60 DAYs + blockTime + 60 * DAY, + 0, // no fuse + renewalControllerAddress, // no controller + { from: ownerAddress }, + ) + + // Predict the node hash of the sub-subnode + const subSubNode = namehash('sub-sub.sub.test') + + // Make a sub-subnode without a renewal controller + await controller.setSubnode( + TEST_SUBNODE, + labelhash('sub-sub'), + dummyAccountAddress, + resolver.address, + // blocktime + 60 DAYs + blockTime + 60 * DAY, + 0, // no fuse + EMPTY_ADDRESS, // no controller + { from: subnodeOwnerAddress }, + ) + + // Make sure the sub-subnode is owned by the dummyAccountAddress + assert.equal(await controller.ownerOf(subSubNode), dummyAccountAddress) + + // Make sure we can renew the sub-subnode using the parent renewal controller + await controller.setExpiry( + TEST_SUBNODE, + labelhash('sub-sub'), + blockTime + 90 * DAY, + { + from: renewalControllerAddress, + }, + ) + + // Make sure the sub-subnode is still owned by the dummyAccountAddress + assert.equal(await controller.ownerOf(subSubNode), dummyAccountAddress) + + // Make sure the expiry of the sub-subnode has been extended by 30 days + assert.equal(await controller.expiryOf(subSubNode), blockTime + 90 * DAY) + }) + + // Make sure that a hacker can't renew a subnode using the renewal controller address. + it('should revert when a hacker tries to renew a subnode using the renewal controller', async () => { + const blockTime = (await ethers.provider.getBlock('latest')).timestamp + + await controller.setSubnode( + TEST_NODE, + labelhash('sub'), + subnodeOwnerAddress, + resolver.address, + // blocktime + 60 DAYs + blockTime + 60 * DAY, + 0, // no fuse + renewalControllerAddress, // no controller + { from: ownerAddress }, + ) + + // Make sure the subnode is owned by the subnodeOwnerAddress + assert.equal(await controller.ownerOf(TEST_SUBNODE), subnodeOwnerAddress) + + // Make sure the renewal controller is set to the renewalControllerAddress + assert.equal( + await controller.renewalControllerOf(TEST_SUBNODE), + renewalControllerAddress, + ) + + // Extend the expiry of the subnode by 30 days by calling the setExpiry + // function from the renewal controller address, expect it to revert + // with custom error, Unauthorised(bytes32 node, address addr); + await expect( + controller.setExpiry( + TEST_NODE, + labelhash('sub'), + blockTime + 90 * DAY, + { + from: hackerAddress, + }, + ), + ).to.be.revertedWith( + `Unauthorised("${TEST_SUBNODE}", "${hackerAddress}")`, + ) + }) }) }) From cdef50efed8e8da56b9766703c02fb6e31e8d0fe Mon Sep 17 00:00:00 2001 From: nxt3d Date: Fri, 23 Feb 2024 06:34:41 -0500 Subject: [PATCH 14/15] make renewal controllers override owners --- contracts/l2/FuseController.sol | 17 ++-- test/l2/TestL2Registry.js | 140 ++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 9 deletions(-) diff --git a/contracts/l2/FuseController.sol b/contracts/l2/FuseController.sol index cd1e49a0a..98565e377 100644 --- a/contracts/l2/FuseController.sol +++ b/contracts/l2/FuseController.sol @@ -415,18 +415,17 @@ contract FuseController is Ownable, IFuseController { tokenDataSubnode ); - // Check to make sure partent cannot control is not burned. - require( - (sub.fuses & PARENT_CANNOT_CONTROL) == 0, - "Parent cannot control" - ); - // Check to make sure the caller is authorized. if ( - // Allow the owner of the parent node to set the expiry. - !(td.owner == msg.sender) && + // Allow the owner of the parent node to set the expiry as + // long as there is no renewal controller set on the parent node. + !(td.owner == msg.sender && + td.renewalController == address(0) && + sub.renewalController == address(0)) && // Allow an authorized user of the parent node to set the expiry. - !(isAuthorized) && + !(isAuthorized && + td.renewalController == address(0) && + sub.renewalController == address(0)) && // Allow the renewal controller of the parent node // as long as the there is no renewal controller set on the subnode // to set the expiry. diff --git a/test/l2/TestL2Registry.js b/test/l2/TestL2Registry.js index 798d60645..35af5c60b 100644 --- a/test/l2/TestL2Registry.js +++ b/test/l2/TestL2Registry.js @@ -87,6 +87,8 @@ describe.only('L2Registry', () => { dummyAccountAddress = await dummyAccount.getAddress() renewalController = await signers[5] renewalControllerAddress = await renewalController.getAddress() + renewalController2 = await signers[6] + renewalControllerAddress2 = await renewalController2.getAddress() resolver = await DelegatableResolver.new() metaDataservice = await StaticMetadataService.new('https://ens.domains') @@ -545,5 +547,143 @@ describe.only('L2Registry', () => { `Unauthorised("${TEST_SUBNODE}", "${hackerAddress}")`, ) }) + + // Make sure that a hacker can't renew a sub-subnode using the parent renewal controller address. + it('should revert when a hacker tries to renew a sub-subnode using the parent renewal controller', async () => { + const blockTime = (await ethers.provider.getBlock('latest')).timestamp + + await controller.setSubnode( + TEST_NODE, + labelhash('sub'), + subnodeOwnerAddress, + resolver.address, + // blocktime + 60 DAYs + blockTime + 60 * DAY, + 0, // no fuse + renewalControllerAddress, // no controller + { from: ownerAddress }, + ) + + // Predict the node hash of the sub-subnode + const subSubNode = namehash('sub-sub.sub.test') + + // Make a sub-subnode without a renewal controller + await controller.setSubnode( + TEST_SUBNODE, + labelhash('sub-sub'), + dummyAccountAddress, + resolver.address, + // blocktime + 60 DAYs + blockTime + 60 * DAY, + 0, // no fuse + EMPTY_ADDRESS, // no controller + { from: subnodeOwnerAddress }, + ) + + // Make sure the sub-subnode is owned by the dummyAccountAddress + assert.equal(await controller.ownerOf(subSubNode), dummyAccountAddress) + + // Make sure we can't renew the sub-subnode using the parent renewal controller + await expect( + controller.setExpiry( + TEST_SUBNODE, + labelhash('sub-sub'), + blockTime + 90 * DAY, + { + from: hackerAddress, + }, + ), + ).to.be.revertedWith(`Unauthorised("${subSubNode}", "${hackerAddress}")`) + }) + + // Make sure that if the renewal controller is set on the sub-subnode that the renewal controller + // on the subnode can't renew the sub-subnode + it('should revert when the subnode renewal controller tries to renew the sub-subnode', async () => { + const blockTime = (await ethers.provider.getBlock('latest')).timestamp + + await controller.setSubnode( + TEST_NODE, + labelhash('sub'), + subnodeOwnerAddress, + resolver.address, + // blocktime + 60 DAYs + blockTime + 60 * DAY, + 0, // no fuse + renewalControllerAddress, // no controller + { from: ownerAddress }, + ) + + // Predict the node hash of the sub-subnode + const subSubNode = namehash('sub-sub.sub.test') + + // Make a sub-subnode with a renewal controller + await controller.setSubnode( + TEST_SUBNODE, + labelhash('sub-sub'), + dummyAccountAddress, + resolver.address, + // blocktime + 60 DAYs + blockTime + 60 * DAY, + 0, // no fuse + renewalControllerAddress2, + { from: subnodeOwnerAddress }, + ) + + // Make sure the sub-subnode is owned by the dummyAccountAddress + assert.equal(await controller.ownerOf(subSubNode), dummyAccountAddress) + + // Make sure we can't renew the sub-subnode using the parent renewal controller + await expect( + controller.setExpiry( + TEST_SUBNODE, + labelhash('sub-sub'), + blockTime + 90 * DAY, + { + from: renewalControllerAddress, + }, + ), + ).to.be.revertedWith( + `Unauthorised("${subSubNode}", "${renewalControllerAddress}")`, + ) + }) + + // Make sure that if PARTENT_CANNOT_CONTROL is set it is still possible for the parent's renewal controller to renew the subnode. + it('should renew a subnode by the owner of the parent when PARTENT_CANNOT_CONTROL is set on the subnode', async () => { + const blockTime = (await ethers.provider.getBlock('latest')).timestamp + + await controller.setSubnode( + TEST_NODE, + labelhash('sub'), + subnodeOwnerAddress, + resolver.address, + // blocktime + 60 DAYs + blockTime + 60 * DAY, + PARENT_CANNOT_CONTROL | CANNOT_BURN_NAME, // no fuse + EMPTY_ADDRESS, // no controller + { from: ownerAddress }, + ) + + // Make sure the subnode is owned by the subnodeOwnerAddress + assert.equal(await controller.ownerOf(TEST_SUBNODE), subnodeOwnerAddress) + + // Extend the expiry of the subnode by 30 days by calling the setExpiry function from the renewal controller address. + await controller.setExpiry( + TEST_NODE, + labelhash('sub'), + blockTime + 90 * DAY, + { + from: ownerAddress, + }, + ) + + // Make sure the subnode is still owned by the subnodeOwnerAddress + assert.equal(await controller.ownerOf(TEST_SUBNODE), subnodeOwnerAddress) + + // Make sure the expiry of the subnode has been extended by 30 days + assert.equal( + await controller.expiryOf(TEST_SUBNODE), + blockTime + 90 * DAY, + ) + }) }) }) From b27533e9db37f9f041c4fb4b78d18dfb0b667ed8 Mon Sep 17 00:00:00 2001 From: nxt3d Date: Fri, 23 Feb 2024 07:10:58 -0500 Subject: [PATCH 15/15] add PARENT_CANNOT_SET_EXPIRY fuse --- contracts/l2/FuseController.sol | 8 ++++---- contracts/l2/IFuseController.sol | 3 ++- test/l2/TestL2Registry.js | 16 +++++++++------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/contracts/l2/FuseController.sol b/contracts/l2/FuseController.sol index 98565e377..b0984086b 100644 --- a/contracts/l2/FuseController.sol +++ b/contracts/l2/FuseController.sol @@ -418,19 +418,19 @@ contract FuseController is Ownable, IFuseController { // Check to make sure the caller is authorized. if ( // Allow the owner of the parent node to set the expiry as - // long as there is no renewal controller set on the parent node. + // the PARENT_CANNOT_SET_EXPIRY fuse is not burned. !(td.owner == msg.sender && td.renewalController == address(0) && - sub.renewalController == address(0)) && + (sub.fuses & PARENT_CANNOT_SET_EXPIRY) == 0) && // Allow an authorized user of the parent node to set the expiry. !(isAuthorized && td.renewalController == address(0) && - sub.renewalController == address(0)) && + (sub.fuses & PARENT_CANNOT_SET_EXPIRY) == 0) && // Allow the renewal controller of the parent node // as long as the there is no renewal controller set on the subnode // to set the expiry. !(td.renewalController == msg.sender && - sub.renewalController == address(0)) && + (sub.fuses & PARENT_CANNOT_SET_EXPIRY) == 0) && // Allow the renewal controller of the subnode to set the expiry. !(sub.renewalController == msg.sender) ) { diff --git a/contracts/l2/IFuseController.sol b/contracts/l2/IFuseController.sol index 4319b11b4..ba0e6f1ab 100644 --- a/contracts/l2/IFuseController.sol +++ b/contracts/l2/IFuseController.sol @@ -11,7 +11,8 @@ uint64 constant CANNOT_TRANSFER = 4; uint64 constant CANNOT_SET_RESOLVER = 8; uint64 constant CANNOT_CREATE_SUBDOMAIN = 16; uint64 constant CANNOT_SET_RENEWAL_CONTROLLER = 32; -uint64 constant PARENT_CANNOT_CONTROL = 64; +uint64 constant PARENT_CANNOT_SET_EXPIRY = 64; +uint64 constant PARENT_CANNOT_CONTROL = 128; interface IFuseController is IController { function expiryOf(bytes32 node) external view returns (uint64); diff --git a/test/l2/TestL2Registry.js b/test/l2/TestL2Registry.js index 35af5c60b..20a948919 100644 --- a/test/l2/TestL2Registry.js +++ b/test/l2/TestL2Registry.js @@ -44,6 +44,7 @@ const { CANNOT_SET_RESOLVER, CANNOT_CREATE_SUBDOMAIN, CANNOT_SET_RENEWAL_CONTROLLER, + PARENT_CANNOT_SET_EXPIRY, PARENT_CANNOT_CONTROL, } = { CAN_DO_EVERYTHING: 0, @@ -53,7 +54,8 @@ const { CANNOT_SET_RESOLVER: 2 ** 3, CANNOT_CREATE_SUBDOMAIN: 2 ** 4, CANNOT_SET_RENEWAL_CONTROLLER: 2 ** 5, - PARENT_CANNOT_CONTROL: 2 ** 6, + PARENT_CANNOT_SET_EXPIRY: 2 ** 6, + PARENT_CANNOT_CONTROL: 2 ** 7, } describe.only('L2Registry', () => { @@ -596,9 +598,9 @@ describe.only('L2Registry', () => { ).to.be.revertedWith(`Unauthorised("${subSubNode}", "${hackerAddress}")`) }) - // Make sure that if the renewal controller is set on the sub-subnode that the renewal controller + // Make sure that if the PARENT_CANNOT_SET_EXPIRY is set on the sub-subnode that the renewal controller // on the subnode can't renew the sub-subnode - it('should revert when the subnode renewal controller tries to renew the sub-subnode', async () => { + it('should revert when the subnode renewal controller tries to renew the sub-subnode and PARENT_CANNOT_SET_EXPIRY is burned', async () => { const blockTime = (await ethers.provider.getBlock('latest')).timestamp await controller.setSubnode( @@ -608,7 +610,7 @@ describe.only('L2Registry', () => { resolver.address, // blocktime + 60 DAYs blockTime + 60 * DAY, - 0, // no fuse + PARENT_CANNOT_CONTROL | CANNOT_BURN_NAME, renewalControllerAddress, // no controller { from: ownerAddress }, ) @@ -624,7 +626,7 @@ describe.only('L2Registry', () => { resolver.address, // blocktime + 60 DAYs blockTime + 60 * DAY, - 0, // no fuse + PARENT_CANNOT_CONTROL | CANNOT_BURN_NAME | PARENT_CANNOT_SET_EXPIRY, renewalControllerAddress2, { from: subnodeOwnerAddress }, ) @@ -647,7 +649,7 @@ describe.only('L2Registry', () => { ) }) - // Make sure that if PARTENT_CANNOT_CONTROL is set it is still possible for the parent's renewal controller to renew the subnode. + // Make sure that if PARTENT_CANNOT_CONTROL is set it is still possible for the parent to renew the subnode. it('should renew a subnode by the owner of the parent when PARTENT_CANNOT_CONTROL is set on the subnode', async () => { const blockTime = (await ethers.provider.getBlock('latest')).timestamp @@ -658,7 +660,7 @@ describe.only('L2Registry', () => { resolver.address, // blocktime + 60 DAYs blockTime + 60 * DAY, - PARENT_CANNOT_CONTROL | CANNOT_BURN_NAME, // no fuse + PARENT_CANNOT_CONTROL | CANNOT_BURN_NAME, EMPTY_ADDRESS, // no controller { from: ownerAddress }, )