diff --git a/contracts/LensHub.sol b/contracts/LensHub.sol index 5148bec..25b415d 100644 --- a/contracts/LensHub.sol +++ b/contracts/LensHub.sol @@ -22,6 +22,10 @@ import {StorageLib} from 'contracts/libraries/StorageLib.sol'; import {FollowLib} from 'contracts/libraries/FollowLib.sol'; import {CollectLib} from 'contracts/libraries/CollectLib.sol'; +///////////////////////////////////// Migration imports //////////////////////////////////// +import {LensHandles} from 'contracts/misc/namespaces/LensHandles.sol'; +import {TokenHandleRegistry} from 'contracts/misc/namespaces/TokenHandleRegistry.sol'; + /** * @title LensHub * @author Lens Protocol @@ -41,6 +45,13 @@ contract LensHub is LensBaseERC721, VersionedInitializable, LensMultiState, Lens address internal immutable FOLLOW_NFT_IMPL; address internal immutable COLLECT_NFT_IMPL; + ///////////////////////////////////// Migration constants //////////////////////////////////// + uint256 internal constant LENS_PROTOCOL_PROFILE_ID = 1; + address internal immutable migrator; + LensHandles internal immutable lensHandles; + TokenHandleRegistry internal immutable tokenHandleRegistry; + ///////////////////////////////// End of migration constants ///////////////////////////////// + /** * @dev This modifier reverts if the caller is not the configured governance address. */ @@ -55,11 +66,20 @@ contract LensHub is LensBaseERC721, VersionedInitializable, LensMultiState, Lens * @param followNFTImpl The follow NFT implementation address. * @param collectNFTImpl The collect NFT implementation address. */ - constructor(address followNFTImpl, address collectNFTImpl) { + constructor( + address followNFTImpl, + address collectNFTImpl, + address migratorAddress, + address lensHandlesAddress, + address tokenHandleRegistryAddress + ) { if (followNFTImpl == address(0)) revert Errors.InitParamsInvalid(); if (collectNFTImpl == address(0)) revert Errors.InitParamsInvalid(); FOLLOW_NFT_IMPL = followNFTImpl; COLLECT_NFT_IMPL = collectNFTImpl; + migrator = migratorAddress; + lensHandles = LensHandles(lensHandlesAddress); + tokenHandleRegistry = TokenHandleRegistry(tokenHandleRegistryAddress); } /// @inheritdoc ILensHub @@ -120,6 +140,49 @@ contract LensHub is LensBaseERC721, VersionedInitializable, LensMultiState, Lens emit Events.CollectModuleWhitelisted(collectModule, whitelist, block.timestamp); } + /////////////////////////////////////////// + /// V1->V2 MIGRATION FUNCTIONS /// + /////////////////////////////////////////// + + function migrateProfile(uint256 profileId, bytes32 handleHash) external { + require(msg.sender == migrator, 'Only migrator'); + delete _profileById[profileId].handleDeprecated; + delete _profileIdByHandleHash[handleHash]; + } + + event ProfileMigrated(uint256 profileId, address profileDestination, string handle, uint256 handleId); + + function _migrateProfilePublic(uint256 profileId) internal { + address profileOwner = StorageLib.getTokenData(profileId).owner; + if (profileOwner != address(0)) { + string memory handle = _profileById[profileId].handleDeprecated; + bytes32 handleHash = keccak256(bytes(handle)); + // "lensprotocol" is the only edge case without .lens suffix: + if (profileId != LENS_PROTOCOL_PROFILE_ID) { + assembly { + let handle_length := mload(handle) + mstore(handle, sub(handle_length, 5)) // Cut 5 chars (.lens) from the end + } + } + delete _profileById[profileId].handleDeprecated; + delete _profileIdByHandleHash[handleHash]; + + uint256 handleId = lensHandles.mintHandle(profileOwner, handle); + tokenHandleRegistry.migrationLinkHandleWithToken(handleId, profileId); + emit ProfileMigrated(profileId, profileOwner, handle, handleId); + } + } + + function batchMigrateProfiles(uint256[] calldata profileIds) external { + for (uint256 i = 0; i < profileIds.length; i++) { + _migrateProfilePublic(profileIds[i]); + } + } + + /////////////////////////////////////////// + /// END OF V1->V2 MIGRATION FUNCTIONS /// + /////////////////////////////////////////// + /////////////////////////////////////////// /// PROFILE OWNER FUNCTIONS /// /////////////////////////////////////////// diff --git a/contracts/libraries/constants/Types.sol b/contracts/libraries/constants/Types.sol index 3d9ab55..c8437bc 100644 --- a/contracts/libraries/constants/Types.sol +++ b/contracts/libraries/constants/Types.sol @@ -74,8 +74,8 @@ library Types { * @param signer The address of the signer. * @param v The signature's recovery parameter. * @param r The signature's r parameter. - * @param s The signature's s parameter - * @param deadline The signature's deadline + * @param s The signature's s parameter. + * @param deadline The signature's deadline. */ struct EIP712Signature { address signer; @@ -90,8 +90,8 @@ library Types { * * @param pubCount The number of publications made to this profile. * @param followModule The address of the current follow module in use by this profile, can be empty. - * @param followNFT The address of the followNFT associated with this profile, can be empty.. - * @param handleDeprecated The deprecated handle slot, no longer used. . + * @param followNFT The address of the followNFT associated with this profile, can be empty. + * @param handleDeprecated The deprecated handle slot, no longer used. * @param imageURI The URI to be used for the profile's image. * @param followNFTURI The URI to be used for the follow NFT. */ diff --git a/contracts/misc/ProfileMigration.sol b/contracts/misc/ProfileMigration.sol new file mode 100644 index 0000000..9289fef --- /dev/null +++ b/contracts/misc/ProfileMigration.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; +import {LensHub} from 'contracts/LensHub.sol'; +import {LensHandles} from 'contracts/misc/namespaces/LensHandles.sol'; +import {TokenHandleRegistry} from 'contracts/misc/namespaces/TokenHandleRegistry.sol'; + +contract ProfileMigration is Ownable { + LensHub public immutable lensHub; + LensHandles public immutable lensHandles; + TokenHandleRegistry public immutable tokenHandleRegistry; + + event ProfileMigrated(uint256 profileId, address profileDestination, string handle, uint256 handleId); + + constructor( + address ownerAddress, + address lensHubAddress, + address lensHandlesAddress, + address tokenHandleRegistryAddress + ) { + Ownable._transferOwnership(ownerAddress); + lensHub = LensHub(lensHubAddress); + lensHandles = LensHandles(lensHandlesAddress); + tokenHandleRegistry = TokenHandleRegistry(tokenHandleRegistryAddress); + } + + struct ProfileMigrationData { + uint256 profileId; + address profileDestination; + string handle; + bytes32 handleHash; + } + + // TODO: Assume we pause everything - creating, transfer, etc. + function _migrateProfile(ProfileMigrationData calldata profileMigrationData) internal { + lensHub.migrateProfile(profileMigrationData.profileId, profileMigrationData.handleHash); + uint256 handleId = lensHandles.mintHandle(profileMigrationData.profileDestination, profileMigrationData.handle); + tokenHandleRegistry.migrationLinkHandleWithToken(handleId, profileMigrationData.profileId); + emit ProfileMigrated( + profileMigrationData.profileId, + profileMigrationData.profileDestination, + profileMigrationData.handle, + handleId + ); + } + + function batchMigrateProfiles(ProfileMigrationData[] calldata profileMigrationDatas) external onlyOwner { + for (uint256 i = 0; i < profileMigrationDatas.length; i++) { + _migrateProfile(profileMigrationDatas[i]); + } + } +} diff --git a/contracts/misc/migrations/ImmutableOwnable.sol b/contracts/misc/migrations/ImmutableOwnable.sol new file mode 100644 index 0000000..dc1ebdd --- /dev/null +++ b/contracts/misc/migrations/ImmutableOwnable.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +contract ImmutableOwnable { + address immutable OWNER; + address immutable LENS_HUB; + + error OnlyOwner(); + error OnlyOwnerOrHub(); + + modifier onlyOwner() { + if (msg.sender != OWNER) { + revert OnlyOwner(); + } + _; + } + + modifier onlyOwnerOrHub() { + if (msg.sender != OWNER && msg.sender != LENS_HUB) { + revert OnlyOwnerOrHub(); + } + _; + } + + constructor(address owner, address lensHub) { + OWNER = owner; + LENS_HUB = lensHub; + } +} diff --git a/contracts/misc/namespaces/Handles.sol b/contracts/misc/namespaces/LensHandles.sol similarity index 80% rename from contracts/misc/namespaces/Handles.sol rename to contracts/misc/namespaces/LensHandles.sol index fae0c91..ccdb01c 100644 --- a/contracts/misc/namespaces/Handles.sol +++ b/contracts/misc/namespaces/LensHandles.sol @@ -3,27 +3,21 @@ pragma solidity ^0.8.19; import {ERC721} from '@openzeppelin/contracts/token/ERC721/ERC721.sol'; -import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; import {VersionedInitializable} from 'contracts/base/upgradeability/VersionedInitializable.sol'; +import {ImmutableOwnable} from 'contracts/misc/migrations/ImmutableOwnable.sol'; library Events { event HandleMinted(string handle, string namespace, uint256 handleId, address to); } -// TODO list: -// 1. Code a contract that can batch-mint those handles -// 2. Code a contract that can batch-link handles to profiles - -contract LensHandles is ERC721, Ownable, VersionedInitializable { +contract LensHandles is ERC721, VersionedInitializable, ImmutableOwnable { // Constant for upgradeability purposes, see VersionedInitializable. Do not confuse with EIP-712 revision number. uint256 internal constant REVISION = 1; string constant NAMESPACE = 'lens'; bytes32 constant NAMESPACE_HASH = keccak256(bytes(NAMESPACE)); - constructor(address owner) ERC721('', '') { - Ownable._transferOwnership(owner); - } + constructor(address owner, address lensHub) ERC721('', '') ImmutableOwnable(owner, lensHub) {} function name() public pure override returns (string memory) { return string.concat(symbol(), ' Handles'); @@ -33,9 +27,7 @@ contract LensHandles is ERC721, Ownable, VersionedInitializable { return string.concat('.', NAMESPACE); } - function initialize(address owner) external initializer { - Ownable._transferOwnership(owner); - } + function initialize() external initializer {} /** * @notice Mints a handle in the given namespace. @@ -45,7 +37,7 @@ contract LensHandles is ERC721, Ownable, VersionedInitializable { * @param to The address where the handle is being minted to. * @param localName The local name of the handle. */ - function mintHandle(address to, string calldata localName) external onlyOwner returns (uint256) { + function mintHandle(address to, string calldata localName) external onlyOwnerOrHub returns (uint256) { bytes32 localNameHash = keccak256(bytes(localName)); bytes32 handleHash = keccak256(abi.encodePacked(localNameHash, NAMESPACE_HASH)); uint256 handleId = uint256(handleHash); diff --git a/contracts/misc/namespaces/TokenHandleRegistry.sol b/contracts/misc/namespaces/TokenHandleRegistry.sol index e0fa3f6..430a912 100644 --- a/contracts/misc/namespaces/TokenHandleRegistry.sol +++ b/contracts/misc/namespaces/TokenHandleRegistry.sol @@ -43,6 +43,9 @@ contract TokenHandleRegistry is VersionedInitializable { address immutable LENS_HUB; address immutable LENS_HANDLES; + // Migration constants + address immutable migrator; + /// 1to1 mapping for now, can be replaced to support multiple handles per token if using mappings /// NOTE: Using bytes32 _handleHash(Handle) and _tokenHash(Token) as keys because solidity doesn't support structs as keys. mapping(bytes32 handle => Token token) handleToToken; @@ -78,13 +81,24 @@ contract TokenHandleRegistry is VersionedInitializable { } // NOTE: We don't need whitelisting yet as we use immutable constants for the first version. - constructor(address lensHub, address lensHandles) { + constructor(address lensHub, address lensHandles, address migratorAddress) { LENS_HUB = lensHub; LENS_HANDLES = lensHandles; + migrator = migratorAddress; } function initialize() external initializer {} + // V1->V2 Migration function + function migrationLinkHandleWithToken(uint256 handleId, uint256 tokenId) external { + require(msg.sender == migrator, 'Only migrator'); + Handle memory handle = Handle({collection: LENS_HANDLES, id: handleId}); + Token memory token = Token({collection: LENS_HUB, id: tokenId}); + handleToToken[_handleHash(handle)] = token; + tokenToHandle[_tokenHash(token)] = handle; + emit Events.HandleLinked(handle, token); + } + // NOTE: Simplified interfaces for the first version - Namespace and LensHub are constants // TODO: Custom logic for linking/unlinking handles and tokens (modules, with bytes passed) function linkHandleWithToken(uint256 handleId, uint256 tokenId) external { diff --git a/test/foundry/Events.t.sol b/test/foundry/Events.t.sol index b1c73f7..854ea8b 100644 --- a/test/foundry/Events.t.sol +++ b/test/foundry/Events.t.sol @@ -39,7 +39,7 @@ contract EventTest is BaseTest { hubProxyAddr = predictContractAddress(deployer, 3); // Deploy implementation contracts. - hubImpl = new LensHub(followNFTAddr, collectNFTAddr); + hubImpl = new LensHub(followNFTAddr, collectNFTAddr, address(0), address(0), address(0)); followNFT = new FollowNFT(hubProxyAddr); collectNFT = new CollectNFT(hubProxyAddr); diff --git a/test/foundry/base/TestSetup.t.sol b/test/foundry/base/TestSetup.t.sol index c6f535f..b7d492f 100644 --- a/test/foundry/base/TestSetup.t.sol +++ b/test/foundry/base/TestSetup.t.sol @@ -168,7 +168,7 @@ contract TestSetup is Test, ForkManagement, ArrayHelpers { hubProxyAddr = computeCreateAddress(deployer, 3); // Deploy implementation contracts. - hubImpl = new LensHub(followNFTAddr, collectNFTAddr); + hubImpl = new LensHub(followNFTAddr, collectNFTAddr, address(0), address(0), address(0)); followNFT = new FollowNFT(hubProxyAddr); collectNFT = new CollectNFT(hubProxyAddr); diff --git a/test/foundry/fork/UpgradeForkTest.t.sol b/test/foundry/fork/UpgradeForkTest.t.sol index 98f43b9..d2e7004 100644 --- a/test/foundry/fork/UpgradeForkTest.t.sol +++ b/test/foundry/fork/UpgradeForkTest.t.sol @@ -355,7 +355,7 @@ contract UpgradeForkTest is BaseTest { address collectNFTAddr = computeCreateAddress(deployer, 2); // Deploy implementation contracts. - hubImpl = new LensHub(followNFTAddr, collectNFTAddr); + hubImpl = new LensHub(followNFTAddr, collectNFTAddr, address(0), address(0), address(0)); followNFT = new FollowNFT(hubProxyAddr); collectNFT = new CollectNFT(hubProxyAddr);