diff --git a/contracts/l2/FuseController.sol b/contracts/l2/FuseController.sol new file mode 100644 index 00000000..b0984086 --- /dev/null +++ b/contracts/l2/FuseController.sol @@ -0,0 +1,684 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.17; + +import "./L2Registry.sol"; +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); + +/** + * @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 (uint64) + * - Byte 80: renewalController (address) + */ +contract FuseController is Ownable, IFuseController { + L2Registry immutable registry; + + IControllerUpgradeTarget upgradeContract; + + // A struct to hold the unpacked data + struct TokenData { + address owner; + address resolver; + uint64 expiry; + uint64 fuses; + address renewalController; + } + + constructor(L2Registry _registry) { + registry = _registry; + } + + /************************* + * IController functions * + *************************/ + + function ownerOfWithData( + bytes calldata tokenData + ) external view returns (address) { + (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)); + + (bool isExpired, address owner, , , , ) = _isExpired(tokenData); + + if (isExpired) { + return address(0); + } + + 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; + + // 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_TRANSFER fuse is not burned. + require((td.fuses & CANNOT_TRANSFER) == 0, "Cannot transfer"); + + return + _pack( + address(this), + 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) { + 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, + 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; + } + return _owner == owner ? 1 : 0; + } + + 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); + } + return resolver; + } + + 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) 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) { + return 0; + } + return fuses; + } + + 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 + ); + + if (isExpired) { + return address(0); + } + 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)); + ( + bool isExpired, + address owner, + address resolver, + uint64 expiry, + uint64 fuses, + address renewalController + ) = _isExpired(tokenData); + + bool isAuthorized = registry.getAuthorization( + uint256(node), + owner, + msg.sender + ); + + if (owner != msg.sender && !isAuthorized) { + revert Unauthorised(node, msg.sender); + } + + if (isExpired) { + revert nameExpired(node); + } + + // Change the controller to the upgrade contract. + registry.setNode( + uint256(node), + _pack( + address(upgradeContract), + owner, + resolver, + expiry, + fuses, + renewalController + ) + ); + + // Call the new contract to notify it of the upgrade. + upgradeContract.upgradeFrom(node, extraData); + } + + /******************* + * 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); + ( + address owner, + , + uint64 expiry, + uint64 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( + address(this), + owner, + newResolver, + expiry, + fuses, + renewalController + ) + ); + } + + // Set the expiry of a subnode, with a node and a label. + function setExpiry( + bytes32 node, + bytes32 labelhash, + uint64 newExpiry + ) external { + TokenData memory td; + TokenData memory sub; + + // get the subnode + bytes32 subnode = keccak256(abi.encodePacked(node, labelhash)); + + // get tokenData + bytes memory tokenData = registry.getData(uint256(node)); + + // Make sure the parent node controller is this contract. + require( + address(_getController(tokenData)) == address(this), + "Controller is not this contract" + ); + + // Make sure the tokenData is 96 bytes long. + require(tokenData.length == 96, "Invalid tokenData length"); + + ( + td.owner, // resolver // expiry + , + , + td.fuses, + td.renewalController + ) = _unpack(tokenData); + + // Make sure the caller is authroized in the parent node. + bool isAuthorized = registry.getAuthorization( + uint256(node), + td.owner, + 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 the caller is authorized. + if ( + // Allow the owner of the parent node to set the expiry as + // the PARENT_CANNOT_SET_EXPIRY fuse is not burned. + !(td.owner == msg.sender && + td.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.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.fuses & PARENT_CANNOT_SET_EXPIRY) == 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), + sub.owner, + sub.resolver, + newExpiry, + sub.fuses, + sub.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, + bytes32 labelhash, + address subnodeOwner, + address subnodeResolver, + uint64 subnodeExpiry, + uint64 subnodeFuses, + address subnodeRenewalController + ) external { + TokenData memory tdNode; + + bytes memory tokenData = registry.getData(uint256(node)); + + // 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, labelhash)); + + // 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), + tdNode.owner, + msg.sender + ); + + if (tdNode.owner != msg.sender && !isAuthorized) { + revert Unauthorised(node, msg.sender); + } + + registry.setSubnode( + uint256(node), + uint256(labelhash), + _pack( + address(this), + subnodeOwner, + subnodeResolver, + subnodeExpiry, + subnodeFusesOld | subnodeFuses, // if there were fuses, then add them to the existing fuses. + subnodeRenewalController + ), + msg.sender, + subnodeOwner + ); + } + + /******************* + * Owner only functions * + *******************/ + + // A function that sets the upgrade contract. + function setUpgradeController( + IControllerUpgradeTarget _upgradeContract + ) external onlyOwner { + upgradeContract = _upgradeContract; + } + + /********************** + * Internal functions * + **********************/ + + 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( + bytes memory tokenData + ) + internal + pure + returns ( + address owner, + address resolver, + uint64 expiry, + 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, 76)) + renewalController := mload(add(tokenData, 96)) + } + } + + function _pack( + address controller, + address owner, + address resolver, + uint64 expiry, + uint64 fuses, + address renewalController + ) internal pure returns (bytes memory /*tokenData*/) { + return + abi.encodePacked( + controller, + owner, + resolver, + expiry, + fuses, + 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/IController.sol b/contracts/l2/IController.sol index 3eb36ffc..07f0eac1 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, @@ -13,9 +17,19 @@ interface IController { uint256 id, uint256 value, bytes calldata data, - bool operatorApproved + 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/IControllerUpgradeTarget.sol b/contracts/l2/IControllerUpgradeTarget.sol new file mode 100644 index 00000000..49b0bb0b --- /dev/null +++ b/contracts/l2/IControllerUpgradeTarget.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.17; + +import "./IController.sol"; + +interface IControllerUpgradeTarget is IController { + function upgradeFrom(bytes32 node, bytes calldata extraData) external; +} diff --git a/contracts/l2/IFuseController.sol b/contracts/l2/IFuseController.sol new file mode 100644 index 00000000..ba0e6f1a --- /dev/null +++ b/contracts/l2/IFuseController.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +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_SET_EXPIRY = 64; +uint64 constant PARENT_CANNOT_CONTROL = 128; + +interface IFuseController is IController { + function expiryOf(bytes32 node) external view returns (uint64); + + function fusesOf(bytes32 node) external view returns (uint64); + + function renewalControllerOf(bytes32 node) external view returns (address); + + function upgrade(bytes32 node, bytes calldata extraData) external; + + function setUpgradeController( + IControllerUpgradeTarget _upgradeController + ) external; +} diff --git a/contracts/l2/L2Registry.sol b/contracts/l2/L2Registry.sol index 611d9335..9a15d3bf 100644 --- a/contracts/l2/L2Registry.sol +++ b/contracts/l2/L2Registry.sol @@ -1,28 +1,62 @@ // SPDX-License-Identifier: MIT 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"; +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 { - mapping(uint256 => bytes) public tokens; +contract L2Registry is Ownable, IERC1155, IERC1155MetadataURI { + using Address for address; + + 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; + + IMetadataService public metadataService; error TokenDoesNotExist(uint256 id); event NewController(uint256 id, address controller); - constructor(bytes memory root) { - tokens[0] = 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; + } + + function getName(uint256 id) external view returns (string memory) { + return tokens[id].name; + } + function safeTransferFrom( address from, address to, @@ -32,6 +66,8 @@ contract L2Registry is IERC1155 { ) 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( @@ -46,6 +82,35 @@ contract L2Registry is IERC1155 { _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 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 { @@ -53,6 +118,27 @@ 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].data).ownerOfWithData( + tokens[id].data + ); + // 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 * *************************/ @@ -60,7 +146,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); @@ -75,7 +161,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], @@ -87,18 +173,51 @@ 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].data).ownerOfWithData( + tokens[id].data + ); + 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 || isApprovedForAll(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].data).ownerOfWithData( + tokens[id].data + ); + return + approvals[owner][delegate] || + tokenApprovals[owner][tokenApprovalsNonce[owner]][id][delegate]; + } + + function getAuthorization( + uint256 id, + address owner, + address delegate + ) public view returns (bool /*authorized*/) { + return + approvals[owner][delegate] || + tokenApprovals[owner][tokenApprovalsNonce[owner]][id][delegate]; } function supportsInterface( @@ -106,17 +225,20 @@ contract L2Registry is IERC1155 { ) external pure returns (bool) { return interfaceId == type(IERC1155).interfaceId || + interfaceId == type(IERC1155MetadataURI).interfaceId || interfaceId == type(IERC165).interfaceId; } - function resolver(uint256 id) external view returns (address) { - bytes memory tokenData = tokens[id]; + 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) { - return _getController(tokens[id]); + function controller( + uint256 id + ) external view returns (IController /*controller*/) { + return _getController(tokens[id].data); } /***************************** @@ -125,7 +247,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); @@ -136,7 +258,7 @@ contract L2Registry is IERC1155 { } // Update the data for this node. - tokens[id] = data; + tokens[id].data = data; } function setSubnode( @@ -147,18 +269,18 @@ 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) - : oldSubnodeController.ownerOf(oldSubnodeData); + : oldSubnodeController.ownerOfWithData(oldSubnodeData); // Get the address of the new controller IController newSubnodeController = _getController(subnodeData); @@ -166,13 +288,23 @@ 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) { - to = _getController(subnodeData).ownerOf(subnodeData); + to = _getController(subnodeData).ownerOfWithData(subnodeData); } + emit TransferSingle(operator, oldOwner, to, subnode, 1); + + _doSafeTransferAcceptanceCheck( + operator, + oldOwner, + to, + subnode, + 1, + bytes("") + ); } /********************** @@ -186,7 +318,7 @@ contract L2Registry is IERC1155 { return IController(address(0)); } assembly { - addr := shr(96, mload(add(data, 32))) + addr := mload(add(data, 20)) } } @@ -197,12 +329,21 @@ contract L2Registry is IERC1155 { uint256 value, bytes calldata data ) internal { - bytes memory tokenData = tokens[id]; + 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)) { 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,13 +352,94 @@ contract L2Registry is IERC1155 { id, value, data, - operatorApproved + isApproved ); - IController newController = _getController(newTokenData); - if (newController != oldController) { - emit NewController(id, address(newController)); + 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, + 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"); + } } - tokens[id] = newTokenData; } } diff --git a/contracts/l2/RootController.sol b/contracts/l2/RootController.sol index 90100ecf..2d07801a 100644 --- a/contracts/l2/RootController.sol +++ b/contracts/l2/RootController.sol @@ -10,23 +10,32 @@ 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; } error CannotTransfer(); + error CannotBurn(); event NewResolver(uint256 id, address resolver); /************************* * 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*/, @@ -40,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, @@ -64,10 +85,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 deleted file mode 100644 index f9c4583b..00000000 --- a/contracts/l2/SimpleController.sol +++ /dev/null @@ -1,111 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.17; - -import "./L2Registry.sol"; -import "./IController.sol"; - -/** - * @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) - */ -contract SimpleController is IController { - L2Registry immutable registry; - - constructor(L2Registry _registry) { - registry = _registry; - } - - /************************* - * IController functions * - *************************/ - function ownerOf(bytes calldata tokenData) external pure returns (address) { - (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) { - (address owner, address resolver) = _unpack(tokenData); - - require(value == 1); - require(from == owner); - require(operator == owner || operatorApproved); - - return _pack(to, resolver); - } - - 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; - } - - /******************* - * Owner functions * - *******************/ - - function setResolver(uint256 id, address newResolver) external { - (address owner, bool authorized) = registry.getAuthorization( - id, - msg.sender - ); - require(owner == msg.sender || authorized); - registry.setNode(id, _pack(owner, newResolver)); - } - - function setSubnode( - uint256 node, - uint256 label, - address subnodeOwner, - address subnodeResolver - ) external { - (address owner, bool authorized) = registry.getAuthorization( - node, - msg.sender - ); - require(owner == msg.sender || authorized); - registry.setSubnode( - node, - label, - _pack(subnodeOwner, subnodeResolver), - msg.sender, - subnodeOwner - ); - } - - function _unpack( - bytes calldata tokenData - ) internal pure returns (address owner, address resolver) { - owner = address(bytes20(tokenData[20:40])); - resolver = address(bytes20(tokenData[40:60])); - } - - function _pack( - address owner, - address resolver - ) internal view returns (bytes memory tokenData) { - tokenData = abi.encodePacked(address(this), owner, resolver); - } -} diff --git a/contracts/l2/mocks/FuseControllerUpgraded.sol b/contracts/l2/mocks/FuseControllerUpgraded.sol new file mode 100644 index 00000000..1a50f75c --- /dev/null +++ b/contracts/l2/mocks/FuseControllerUpgraded.sol @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.17; + +import "../L2Registry.sol"; +import "../IFuseController.sol"; +import "../IControllerUpgradeTarget.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. + * 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 (uint64) + * - Byte 80: renewalController (address) + */ +contract FuseControllerUpgraded is + Ownable, + IFuseController, + IControllerUpgradeTarget +{ + L2Registry immutable registry; + + IControllerUpgradeTarget upgradeContract; + + // A struct to hold the unpacked data + struct TokenData { + address owner; + address resolver; + uint64 expiry; + uint64 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)); + (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 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, + 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 (uint64) { + // get the tokenData + bytes memory tokenData = registry.getData(uint256(node)); + (, , , uint64 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, + uint64 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, + uint64 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, + uint64 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( + IControllerUpgradeTarget _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, + uint64 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, + uint64 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 d255ec02..20a94891 100644 --- a/test/l2/TestL2Registry.js +++ b/test/l2/TestL2Registry.js @@ -1,14 +1,64 @@ +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 SimpleController = artifacts.require('SimpleController.sol') -const { labelhash, namehash, encodeName, FUSES } = require('../test-utils/ens') -const ROOT_NODE = namehash('') +const FuseController = artifacts.require('FuseController.sol') +const FuseControllerUpgraded = artifacts.require('FuseControllerUpgraded.sol') +const StaticMetadataService = artifacts.require('StaticMetadataService.sol') 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') + +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 -contract('L2Registry', function (accounts) { +function increaseTime(delay) { + return ethers.provider.send('evm_increaseTime', [delay]) +} + +function mine() { + return ethers.provider.send('evm_mine') +} + +const { + CAN_DO_EVERYTHING, + CANNOT_BURN_NAME, + CANNOT_BURN_FUSES, + CANNOT_TRANSFER, + CANNOT_SET_RESOLVER, + CANNOT_CREATE_SUBDOMAIN, + CANNOT_SET_RENEWAL_CONTROLLER, + PARENT_CANNOT_SET_EXPIRY, + PARENT_CANNOT_CONTROL, +} = { + CAN_DO_EVERYTHING: 0, + 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_SET_EXPIRY: 2 ** 6, + PARENT_CANNOT_CONTROL: 2 ** 7, +} + +describe.only('L2Registry', () => { let signers, deployer, deployerAddress, @@ -17,7 +67,14 @@ contract('L2Registry', function (accounts) { resolver, root, registry, - controller + controller, + controllerUpgraded, + dummyAddress, + operator, + delegate, + metaDataservice + let MAX_EXPIRY = 2n ** 64n - 1n + beforeEach(async () => { signers = await ethers.getSigners() deployer = await signers[0] @@ -26,37 +83,609 @@ contract('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() + 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') root = await RootController.new(resolver.address) - registry = await L2Registry.new(root.address) - controller = await SimpleController.new(registry.address) - }) - it('should deploy', async () => { + 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] + 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) + + const testNodeData = ethers.utils.solidityPack( + ['address', 'address', 'address', 'uint64', 'uint64', 'address'], + [ + controller.address, + ownerAddress, + resolver.address, + MAX_EXPIRY, + CANNOT_BURN_NAME | PARENT_CANNOT_CONTROL, + EMPTY_ADDRESS, + ], + ) + 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'], - [controller.address, ownerAddress, resolver.address], - ), + testNodeData, ) + 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) - - await controller.setSubnode( - TEST_NODE, - labelhash('sub'), - subnodeOwnerAddress, - resolver.address, - { from: ownerAddress }, + assert.equal(await controller.ownerOf(TEST_NODE), ownerAddress) + assert.equal(await controller.expiryOf(TEST_NODE), MAX_EXPIRY) + assert.equal( + await controller.fusesOf(TEST_NODE), + CANNOT_BURN_NAME | PARENT_CANNOT_CONTROL, ) - 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.renewalControllerOf(TEST_NODE), EMPTY_ADDRESS) + }) + + beforeEach(async () => { + result = await ethers.provider.send('evm_snapshot') + }) + afterEach(async () => { + await ethers.provider.send('evm_revert', [result]) + }) + + describe('??()', () => { + 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, + labelhash('sub'), + subnodeOwnerAddress, + resolver.address, + MAX_EXPIRY, + CANNOT_SET_RESOLVER | CANNOT_BURN_NAME | PARENT_CANNOT_CONTROL, // 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), + CANNOT_SET_RESOLVER | CANNOT_BURN_NAME | PARENT_CANNOT_CONTROL, + ) + assert.equal( + await controller.renewalControllerOf(TEST_SUBNODE), + EMPTY_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, + ) + }) + + // 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 + + // 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, + ) + + console.log('blockTime', blockTime) + + await increaseTime(60 * DAY) + await mine() + + //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, + ) + }) + + // 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('') + }) + + // 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, + ) + 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 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, + ) + }) + + // 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}")`, + ) + }) + + // 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 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 and PARENT_CANNOT_SET_EXPIRY is burned', 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, + 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, + PARENT_CANNOT_CONTROL | CANNOT_BURN_NAME | PARENT_CANNOT_SET_EXPIRY, + 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 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, + 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, + ) + }) }) })