diff --git a/src/JBProjectHandles.sol b/src/JBProjectHandles.sol index 24bdf24..1e1fd9f 100644 --- a/src/JBProjectHandles.sol +++ b/src/JBProjectHandles.sol @@ -25,13 +25,12 @@ contract JBProjectHandles is IJBProjectHandles, ERC2771Context { //*********************************************************************// /// @notice The key of the ENS text record. - string public constant override TEXT_KEY_PREFIX = "juicebox"; - string public constant override TEXT_KEY_PROJECT_ID_SUFFIX = ":projectId"; - string public constant override TEXT_KEY_CHAIN_ID_SUFFIX = ":chainId"; + string public constant override TEXT_KEY = "juicebox"; /// @notice The ENS registry contract address. /// @dev Same on every network - ENS public constant ENS_REGISTRY = ENS(0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e); + ENS public constant ENS_REGISTRY = + ENS(0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e); //*********************************************************************// // --------------- public immutable stored properties ---------------- // @@ -49,8 +48,8 @@ contract JBProjectHandles is IJBProjectHandles, ERC2771Context { /// @custom:param chainId The chain ID of the network on which the project ID exists. /// @custom:param projectId The ID of the project to get an ENS name for. /// @custom:param projectOwner The address of the project's owner. - mapping(uint256 chainId => mapping(uint256 projectId => mapping(address projectOwner => string[] ensParts))) private - _ensNamePartsOf; + mapping(uint256 chainId => mapping(uint256 projectId => mapping(address projectOwner => string[] ensParts))) + private _ensNamePartsOf; //*********************************************************************// // ------------------------- external views -------------------------- // @@ -67,14 +66,11 @@ contract JBProjectHandles is IJBProjectHandles, ERC2771Context { uint256 chainId, uint256 projectId, address projectOwner - ) - external - view - override - returns (string memory) - { + ) external view override returns (string memory) { // Get a reference to the project's ENS name parts. - string[] memory ensNameParts = _ensNamePartsOf[chainId][projectId][projectOwner]; + string[] memory ensNameParts = _ensNamePartsOf[chainId][projectId][ + projectOwner + ]; // Return an empty string if not found. if (ensNameParts.length == 0) return ""; @@ -89,17 +85,25 @@ contract JBProjectHandles is IJBProjectHandles, ERC2771Context { if (textResolver == address(0)) return ""; // Find the projectId that the text record of the ENS name is mapped to. - string memory textRecordProjectId = - ITextResolver(textResolver).text(hashedName, string.concat(TEXT_KEY_PREFIX, TEXT_KEY_PROJECT_ID_SUFFIX)); - - // Find the chainId that the text record of the ENS name is mapped to. - string memory textRecordChainId = - ITextResolver(textResolver).text(hashedName, string.concat(TEXT_KEY_PREFIX, TEXT_KEY_CHAIN_ID_SUFFIX)); + string memory textRecord = ITextResolver(textResolver).text( + hashedName, + TEXT_KEY + ); // Return empty string if text record from ENS name doesn't match projectId or chainId. if ( - keccak256(bytes(textRecordProjectId)) != keccak256(bytes(Strings.toString(projectId))) - || keccak256(bytes(textRecordChainId)) != keccak256(bytes(Strings.toString(chainId))) + keccak256(bytes(textRecord)) != + keccak256( + bytes( + string.concat( + Strings.toString(chainId), + ":", + Strings.toString(projectId), + ":", + Strings.toHexString(uint256(uint160(projectOwner))) + ) + ) + ) ) return ""; // Format the handle from the name parts. @@ -115,12 +119,7 @@ contract JBProjectHandles is IJBProjectHandles, ERC2771Context { uint256 chainId, uint256 projectId, address projectOwner - ) - external - view - override - returns (string[] memory) - { + ) external view override returns (string[] memory) { return _ensNamePartsOf[chainId][projectId][projectOwner]; } @@ -130,7 +129,10 @@ contract JBProjectHandles is IJBProjectHandles, ERC2771Context { /// @param projects A contract which mints ERC-721's that represent project ownership and transfers. /// @param trustedForwarder The trusted forwarder for the ERC2771Context. - constructor(IJBProjects projects, address trustedForwarder) ERC2771Context(trustedForwarder) { + constructor( + IJBProjects projects, + address trustedForwarder + ) ERC2771Context(trustedForwarder) { PROJECTS = projects; } @@ -144,7 +146,11 @@ contract JBProjectHandles is IJBProjectHandles, ERC2771Context { /// @param chainId The chain ID of the network on which the project ID exists. /// @param projectId The ID of the project to set an ENS handle for. /// @param parts The parts of the ENS domain to use as the project handle, excluding the trailing .eth. - function setEnsNamePartsFor(uint256 chainId, uint256 projectId, string[] memory parts) external override { + function setEnsNamePartsFor( + uint256 chainId, + uint256 projectId, + string[] memory parts + ) external override { // Get a reference to the number of parts are in the ENS name. uint256 partsLength = parts.length; @@ -159,7 +165,12 @@ contract JBProjectHandles is IJBProjectHandles, ERC2771Context { // Store the parts. _ensNamePartsOf[chainId][projectId][_msgSender()] = parts; - emit SetEnsNameParts(projectId, _formatHandle(parts), parts, _msgSender()); + emit SetEnsNameParts( + projectId, + _formatHandle(parts), + parts, + _msgSender() + ); } //*********************************************************************// @@ -169,14 +180,18 @@ contract JBProjectHandles is IJBProjectHandles, ERC2771Context { /// @notice Formats ENS name parts into a handle. /// @param ensNameParts The ENS name parts to format into a handle. /// @return handle The formatted ENS handle. - function _formatHandle(string[] memory ensNameParts) internal pure returns (string memory handle) { + function _formatHandle( + string[] memory ensNameParts + ) internal pure returns (string memory handle) { // Get a reference to the number of parts are in the ENS name. uint256 partsLength = ensNameParts.length; // Concatenate each name part. for (uint256 i = 1; i <= partsLength; i++) { // Compute the handle. - handle = string(abi.encodePacked(handle, ensNameParts[partsLength - i])); + handle = string( + abi.encodePacked(handle, ensNameParts[partsLength - i]) + ); // Add a dot if this part isn't the last. if (i < partsLength) handle = string(abi.encodePacked(handle, ".")); @@ -187,16 +202,25 @@ contract JBProjectHandles is IJBProjectHandles, ERC2771Context { /// @dev See https://eips.ethereum.org/EIPS/eip-137. /// @param ensNameParts The parts of an ENS name to hash. /// @return namehash The namehash for an ENS name parts. - function _namehash(string[] memory ensNameParts) internal pure returns (bytes32 namehash) { + function _namehash( + string[] memory ensNameParts + ) internal pure returns (bytes32 namehash) { // Hash the trailing "eth" suffix. - namehash = keccak256(abi.encodePacked(namehash, keccak256(abi.encodePacked("eth")))); + namehash = keccak256( + abi.encodePacked(namehash, keccak256(abi.encodePacked("eth"))) + ); // Get a reference to the number of parts are in the ENS name. uint256 nameLength = ensNameParts.length; // Hash each part. for (uint256 i; i < nameLength; i++) { - namehash = keccak256(abi.encodePacked(namehash, keccak256(abi.encodePacked(ensNameParts[i])))); + namehash = keccak256( + abi.encodePacked( + namehash, + keccak256(abi.encodePacked(ensNameParts[i])) + ) + ); } } @@ -213,7 +237,13 @@ contract JBProjectHandles is IJBProjectHandles, ERC2771Context { } /// @dev ERC-2771 specifies the context as being a single address (20 bytes). - function _contextSuffixLength() internal view virtual override returns (uint256) { + function _contextSuffixLength() + internal + view + virtual + override + returns (uint256) + { return super._contextSuffixLength(); } } diff --git a/src/interfaces/IJBProjectHandles.sol b/src/interfaces/IJBProjectHandles.sol index df30b7c..167f9be 100644 --- a/src/interfaces/IJBProjectHandles.sol +++ b/src/interfaces/IJBProjectHandles.sol @@ -5,26 +5,32 @@ import "@ensdomains/ens-contracts/contracts/resolvers/profiles/ITextResolver.sol import "@bananapus/core/src/interfaces/IJBProjects.sol"; interface IJBProjectHandles { - event SetEnsNameParts(uint256 indexed projectId, string indexed handle, string[] parts, address caller); - - function setEnsNamePartsFor(uint256 chainId, uint256 projectId, string[] memory parts) external; + event SetEnsNameParts( + uint256 indexed projectId, + string indexed handle, + string[] parts, + address caller + ); + + function setEnsNamePartsFor( + uint256 chainId, + uint256 projectId, + string[] memory parts + ) external; function ensNamePartsOf( uint256 chainId, uint256 projectId, address projectOwner - ) - external - view - returns (string[] memory); - - function TEXT_KEY_PREFIX() external view returns (string memory); + ) external view returns (string[] memory); - function TEXT_KEY_PROJECT_ID_SUFFIX() external view returns (string memory); - - function TEXT_KEY_CHAIN_ID_SUFFIX() external view returns (string memory); + function TEXT_KEY() external view returns (string memory); function PROJECTS() external view returns (IJBProjects); - function handleOf(uint256 chainId, uint256 projectId, address projectOwner) external view returns (string memory); + function handleOf( + uint256 chainId, + uint256 projectId, + address projectOwner + ) external view returns (string memory); } diff --git a/test/JBProjectHandles.t.sol b/test/JBProjectHandles.t.sol index 4c2bc26..93b8707 100644 --- a/test/JBProjectHandles.t.sol +++ b/test/JBProjectHandles.t.sol @@ -13,11 +13,18 @@ import "../src/JBProjectHandles.sol"; import {JBPermissionIds} from "@bananapus/permission-ids/src/JBPermissionIds.sol"; ENS constant ensRegistry = ENS(0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e); -IJBProjectHandles constant oldHandle = IJBProjectHandles(0x41126eC99F8A989fEB503ac7bB4c5e5D40E06FA4); +IJBProjectHandles constant oldHandle = IJBProjectHandles( + 0x41126eC99F8A989fEB503ac7bB4c5e5D40E06FA4 +); contract ContractTest is Test { // For testing the event emitted - event SetEnsNameParts(uint256 indexed projectId, string indexed ensName, string[] parts, address caller); + event SetEnsNameParts( + uint256 indexed projectId, + string indexed ensName, + string[] parts, + address caller + ); address projectOwner = address(6_942_069); @@ -41,7 +48,9 @@ contract ContractTest is Test { // ------------------------ SetEnsNamePartsFor(..) ------------------- // //*********************************************************************// - function testSetEnsNamePartsFor_passIfCallerIsProjectOwnerAndOnlyName(string calldata name) public { + function testSetEnsNamePartsFor_passIfCallerIsProjectOwnerAndOnlyName( + string calldata name + ) public { vm.assume(bytes(name).length != 0); uint256 projectId = jbProjects.createFor(projectOwner); @@ -58,17 +67,22 @@ contract ContractTest is Test { projectHandle.setEnsNamePartsFor(chainId, projectId, nameParts); // Control: correct ENS name? - assertEq(projectHandle.ensNamePartsOf(chainId, projectId, projectOwner), nameParts); + assertEq( + projectHandle.ensNamePartsOf(chainId, projectId, projectOwner), + nameParts + ); } function testSetEnsNameWithSubdomainFor_passIfMultipleSubdomainLevels( string memory name, string memory subdomain, string memory subsubdomain - ) - public - { - vm.assume(bytes(name).length > 0 && bytes(subdomain).length > 0 && bytes(subsubdomain).length > 0); + ) public { + vm.assume( + bytes(name).length > 0 && + bytes(subdomain).length > 0 && + bytes(subsubdomain).length > 0 + ); uint256 projectId = jbProjects.createFor(projectOwner); uint256 chainId = 1; @@ -79,7 +93,9 @@ contract ContractTest is Test { nameParts[1] = subdomain; nameParts[2] = name; - string memory fullName = string(abi.encodePacked(name, ".", subdomain, ".", subsubdomain)); + string memory fullName = string( + abi.encodePacked(name, ".", subdomain, ".", subsubdomain) + ); // Test event vm.expectEmit(true, true, true, true); @@ -89,17 +105,22 @@ contract ContractTest is Test { projectHandle.setEnsNamePartsFor(chainId, projectId, nameParts); // Control: ENS has correct name and domain - assertEq(projectHandle.ensNamePartsOf(chainId, projectId, projectOwner), nameParts); + assertEq( + projectHandle.ensNamePartsOf(chainId, projectId, projectOwner), + nameParts + ); } function testSetEnsNameWithSubdomainFor_RevertIfEmptyElementInNameParts( string memory name, string memory subdomain, string memory subsubdomain - ) - public - { - vm.assume(bytes(name).length == 0 || bytes(subdomain).length == 0 || bytes(subsubdomain).length == 0); + ) public { + vm.assume( + bytes(name).length == 0 || + bytes(subdomain).length == 0 || + bytes(subsubdomain).length == 0 + ); uint256 projectId = jbProjects.createFor(projectOwner); uint256 chainId = 1; @@ -115,7 +136,10 @@ contract ContractTest is Test { projectHandle.setEnsNamePartsFor(chainId, projectId, nameParts); // Control: ENS has correct name and domain - assertEq(projectHandle.ensNamePartsOf(chainId, projectId, projectOwner), new string[](0)); + assertEq( + projectHandle.ensNamePartsOf(chainId, projectId, projectOwner), + new string[](0) + ); } function testSetEnsNameWithSubdomainFor_RevertIfEmptyNameParts() public { @@ -130,18 +154,27 @@ contract ContractTest is Test { projectHandle.setEnsNamePartsFor(chainId, projectId, nameParts); // Control: ENS has correct name and domain - assertEq(projectHandle.ensNamePartsOf(chainId, projectId, projectOwner), new string[](0)); + assertEq( + projectHandle.ensNamePartsOf(chainId, projectId, projectOwner), + new string[](0) + ); } //*********************************************************************// // ---------------------------- handleOf(..) ------------------------- // //*********************************************************************// - function testHandleOf_returnsEmptyStringIfNoHandleSet(uint256 chainId, uint256 projectId) public { + function testHandleOf_returnsEmptyStringIfNoHandleSet( + uint256 chainId, + uint256 projectId + ) public { // No handle set on the previous JBProjectHandle version neither vm.mockCall( address(oldHandle), - abi.encodeCall(IJBProjectHandles.ensNamePartsOf, (chainId, projectId, projectOwner)), + abi.encodeCall( + IJBProjectHandles.ensNamePartsOf, + (chainId, projectId, projectOwner) + ), abi.encode(new string[](0)) ); @@ -152,15 +185,17 @@ contract ContractTest is Test { string calldata name, string calldata subdomain, string calldata subsubdomain - ) - public - { - vm.assume(bytes(name).length > 0 && bytes(subdomain).length > 0 && bytes(subsubdomain).length > 0); + ) public { + vm.assume( + bytes(name).length > 0 && + bytes(subdomain).length > 0 && + bytes(subsubdomain).length > 0 + ); uint256 projectId = jbProjects.createFor(projectOwner); uint256 chainId = 1; - string memory KEY = projectHandle.TEXT_KEY_PREFIX(); + string memory KEY = projectHandle.TEXT_KEY(); // name.subdomain.subsubdomain.eth is stored as ['subsubdomain', 'subdomain', 'domain'] string[] memory nameParts = new string[](3); @@ -185,20 +220,29 @@ contract ContractTest is Test { vm.mockCall( address(ensTextResolver), - abi.encodeWithSelector(ITextResolver.text.selector, _namehash(nameParts), string.concat(KEY, ":projectId")), - abi.encode(Strings.toString(projectId)) - ); - - vm.mockCall( - address(ensTextResolver), - abi.encodeWithSelector(ITextResolver.text.selector, _namehash(nameParts), string.concat(KEY, ":chainId")), - abi.encode(Strings.toString(chainId)) + abi.encodeWithSelector( + ITextResolver.text.selector, + _namehash(nameParts), + KEY + ), + abi.encode( + string.concat( + Strings.toString(chainId), + ":", + Strings.toString(projectId), + ":", + Strings.toHexString(uint256(uint160(projectOwner))) + ) + ) ); // Mock the registration on the previous version vm.mockCall( address(oldHandle), - abi.encodeCall(IJBProjectHandles.ensNamePartsOf, (chainId, projectId, projectOwner)), + abi.encodeCall( + IJBProjectHandles.ensNamePartsOf, + (chainId, projectId, projectOwner) + ), abi.encode(oldNamePart) ); @@ -216,15 +260,16 @@ contract ContractTest is Test { string calldata name, string calldata subdomain, string calldata subsubdomain - ) - public - { + ) public { vm.assume(projectId != reverseId); // No handle set on the previous JBProjectHandle version vm.mockCall( address(oldHandle), - abi.encodeCall(IJBProjectHandles.ensNamePartsOf, (chainId, projectId, projectOwner)), + abi.encodeCall( + IJBProjectHandles.ensNamePartsOf, + (chainId, projectId, projectOwner) + ), abi.encode(new string[](0)) ); @@ -250,19 +295,20 @@ contract ContractTest is Test { string calldata name, string calldata subdomain, string calldata subsubdomain - ) - public - { + ) public { vm.assume(projectId != reverseId); // No handle set on the previous JBProjectHandle version vm.mockCall( address(oldHandle), - abi.encodeCall(IJBProjectHandles.ensNamePartsOf, (chainId, projectId, projectOwner)), + abi.encodeCall( + IJBProjectHandles.ensNamePartsOf, + (chainId, projectId, projectOwner) + ), abi.encode(new string[](0)) ); - string memory KEY = projectHandle.TEXT_KEY_PREFIX(); + string memory KEY = projectHandle.TEXT_KEY(); // name.subdomain.subsubdomain.eth is stored as ['subsubdomain', 'subdomain', 'domain'] string[] memory nameParts = new string[](3); @@ -278,7 +324,11 @@ contract ContractTest is Test { vm.mockCall( address(ensTextResolver), - abi.encodeWithSelector(ITextResolver.text.selector, _namehash(nameParts), KEY), + abi.encodeWithSelector( + ITextResolver.text.selector, + _namehash(nameParts), + KEY + ), abi.encode(Strings.toString(reverseId)) ); @@ -289,15 +339,17 @@ contract ContractTest is Test { string calldata name, string calldata subdomain, string calldata subsubdomain - ) - public - { - vm.assume(bytes(name).length > 0 && bytes(subdomain).length > 0 && bytes(subsubdomain).length > 0); + ) public { + vm.assume( + bytes(name).length > 0 && + bytes(subdomain).length > 0 && + bytes(subsubdomain).length > 0 + ); uint256 projectId = jbProjects.createFor(projectOwner); uint256 chainId = 1; - string memory KEY = projectHandle.TEXT_KEY_PREFIX(); + string memory KEY = projectHandle.TEXT_KEY(); // name.subdomain.subsubdomain.eth is stored as ['subsubdomain', 'subdomain', 'domain'] string[] memory nameParts = new string[](3); @@ -316,14 +368,20 @@ contract ContractTest is Test { vm.mockCall( address(ensTextResolver), - abi.encodeWithSelector(ITextResolver.text.selector, _namehash(nameParts), string.concat(KEY, ":projectId")), - abi.encode(Strings.toString(projectId)) - ); - - vm.mockCall( - address(ensTextResolver), - abi.encodeWithSelector(ITextResolver.text.selector, _namehash(nameParts), string.concat(KEY, ":chainId")), - abi.encode(Strings.toString(chainId)) + abi.encodeWithSelector( + ITextResolver.text.selector, + _namehash(nameParts), + KEY + ), + abi.encode( + string.concat( + Strings.toString(chainId), + ":", + Strings.toString(projectId), + ":", + Strings.toHexString(uint256(uint160(projectOwner))) + ) + ) ); assertEq( @@ -344,15 +402,24 @@ contract ContractTest is Test { } } - function _namehash(string[] memory ensName) internal pure returns (bytes32 namehash) { - namehash = keccak256(abi.encodePacked(namehash, keccak256(abi.encodePacked("eth")))); + function _namehash( + string[] memory ensName + ) internal pure returns (bytes32 namehash) { + namehash = keccak256( + abi.encodePacked(namehash, keccak256(abi.encodePacked("eth"))) + ); // Get a reference to the number of parts are in the ENS name. uint256 nameLength = ensName.length; // Hash each part. for (uint256 i = 0; i < nameLength; i++) { - namehash = keccak256(abi.encodePacked(namehash, keccak256(abi.encodePacked(ensName[i])))); + namehash = keccak256( + abi.encodePacked( + namehash, + keccak256(abi.encodePacked(ensName[i])) + ) + ); } } }