diff --git a/.gitignore b/.gitignore index 5a77d23..afe93e4 100644 --- a/.gitignore +++ b/.gitignore @@ -24,8 +24,6 @@ coverage.json .coverage_contracts lcov* -addresses.json - # Compiler files forge-cache/ out/ diff --git a/addresses.json b/addresses.json new file mode 100644 index 0000000..6b3accf --- /dev/null +++ b/addresses.json @@ -0,0 +1,46 @@ +{ + "mainnet": { + "chainId": 137, + "network": "polygon", + "LensHubProxy": "0xDb46d1Dc155634FbC732f92E853b10B288AD5a1d", + "LensHubImplementation": "0x96f1ba24294ffe0dfcd832d8376da4a4645a4cd6", + "ModuleGlobals": "0x3Df697FF746a60CBe9ee8D47555c88CB66f03BB9", + "FreeCollectModule": "0x23b9467334bEb345aAa6fd1545538F3d54436e96", + "PoolAddressesProvider": "0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb", + "MultirecipientFeeCollectModule": "0xfa9da21d0a18c7b7de4566481c1e8952371f880a", + "StepwiseCollectModule": "0x210c94877dcfceda8238acd382372278844a54d7", + "ERC4626FeeCollectModule": "0x394fd100b13197787023ef4cf318cde456545db7", + "AaveFeeCollectModule": "0xa94713d0688c8a483c3352635cec4e0ce88d6a29", + "TokenGatedReferenceModule": "0x3d7f4f71a90fe5a9d13fab2716080f2917cf88f3" + }, + "testnet": { + "chainId": 80001, + "network": "mumbai", + "LensHubProxy": "0x60Ae865ee4C725cd04353b5AAb364553f56ceF82", + "LensHubImplementation": "0x45cf9Ba12b43F6c8B7148E06A6f84c5B9ad3Dd44", + "ModuleGlobals": "0x1353aAdfE5FeD85382826757A95DE908bd21C4f9", + "FreeCollectModule": "0x0BE6bD7092ee83D44a6eC1D949626FeE48caB30c", + "PoolAddressesProvider": "0x5343b5bA672Ae99d627A1C87866b8E53F47Db2E6", + "MultirecipientFeeCollectModule": "0x99d6c3eabf05435e851c067d2c3222716f7fcfe5", + "StepwiseCollectModule": "0x7a7b8e7699e0492da1d3c7eab7e2f3bf1065aa40", + "ERC4626FeeCollectModule": "0x79697402bd2caa19a53d615fb1a30a98e35b84d5", + "AaveFeeCollectModule": "0x912860ed4ed6160c48a52d52fcab5c059d34fe5a", + "TokenGatedReferenceModule": "0xb4ba8dccd35bd3dcc5d58dbb9c7dff9c9268add9", + "InteractionLogic": "0x845242e2Cd249af8D4f0D7085DefEAc3381815E3" + }, + "sandbox": { + "chainId": 80001, + "network": "mumbai", + "LensHubProxy": "0x7582177F9E536aB0b6c721e11f383C326F2Ad1D5", + "LensHubImplementation": "0x1b30F214c192EF4B7F8c9926c47C4161016955DA", + "ModuleGlobals": "0xcbCC5b9611d22d11403373432642Df9Ef7Dd81AD", + "FreeCollectModule": "0x11C45Cbc6fDa2dbe435C0079a2ccF9c4c7051595", + "PoolAddressesProvider": "0x5343b5bA672Ae99d627A1C87866b8E53F47Db2E6", + "MultirecipientFeeCollectModule": "0x1cff6c45b0de2fff70670ef4dc67a92a1ccfe0bb", + "StepwiseCollectModule": "0x6928d6127dfa0da401737e6ff421fcf62d5617a3", + "ERC4626FeeCollectModule": "0x31126c602cf88193825a99dcd1d17bf1124b1b4f", + "AaveFeeCollectModule": "0x666e06215747879ee68b3e5a317dcd8411de1897", + "TokenGatedReferenceModule": "0x86d35562ceb9f10d7c2c23c098dfeacb02f53853", + "MockSandboxGovernance": "0x1677d9cC4861f1C85ac7009d5F06f49c928CA2AD" + } +} diff --git a/contracts/misc/migrations/ImmutableOwnable.sol b/contracts/misc/migrations/ImmutableOwnable.sol index dc1ebdd..1cfac76 100644 --- a/contracts/misc/migrations/ImmutableOwnable.sol +++ b/contracts/misc/migrations/ImmutableOwnable.sol @@ -5,9 +5,11 @@ pragma solidity ^0.8.19; contract ImmutableOwnable { address immutable OWNER; address immutable LENS_HUB; + address immutable MIGRATOR; error OnlyOwner(); error OnlyOwnerOrHub(); + error OnlyOwnerOrHubOrMigrator(); modifier onlyOwner() { if (msg.sender != OWNER) { @@ -23,8 +25,16 @@ contract ImmutableOwnable { _; } - constructor(address owner, address lensHub) { + modifier onlyOwnerOrHubOrMigrator() { + if (msg.sender != OWNER && msg.sender != LENS_HUB && msg.sender != MIGRATOR) { + revert OnlyOwnerOrHubOrMigrator(); + } + _; + } + + constructor(address owner, address lensHub, address migrator) { OWNER = owner; LENS_HUB = lensHub; + MIGRATOR = migrator; } } diff --git a/contracts/misc/ProfileMigration.sol b/contracts/misc/migrations/ProfileMigration.sol similarity index 92% rename from contracts/misc/ProfileMigration.sol rename to contracts/misc/migrations/ProfileMigration.sol index 9289fef..e388db4 100644 --- a/contracts/misc/ProfileMigration.sol +++ b/contracts/misc/migrations/ProfileMigration.sol @@ -7,6 +7,13 @@ import {LensHub} from 'contracts/LensHub.sol'; import {LensHandles} from 'contracts/misc/namespaces/LensHandles.sol'; import {TokenHandleRegistry} from 'contracts/misc/namespaces/TokenHandleRegistry.sol'; +struct ProfileMigrationData { + uint256 profileId; + address profileDestination; + string handle; + bytes32 handleHash; +} + contract ProfileMigration is Ownable { LensHub public immutable lensHub; LensHandles public immutable lensHandles; @@ -26,13 +33,6 @@ contract ProfileMigration is Ownable { 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); diff --git a/contracts/misc/namespaces/LensHandles.sol b/contracts/misc/namespaces/LensHandles.sol index ccdb01c..f6f7dc1 100644 --- a/contracts/misc/namespaces/LensHandles.sol +++ b/contracts/misc/namespaces/LensHandles.sol @@ -6,7 +6,7 @@ import {ERC721} from '@openzeppelin/contracts/token/ERC721/ERC721.sol'; import {VersionedInitializable} from 'contracts/base/upgradeability/VersionedInitializable.sol'; import {ImmutableOwnable} from 'contracts/misc/migrations/ImmutableOwnable.sol'; -library Events { +library HandlesEvents { event HandleMinted(string handle, string namespace, uint256 handleId, address to); } @@ -17,7 +17,11 @@ contract LensHandles is ERC721, VersionedInitializable, ImmutableOwnable { string constant NAMESPACE = 'lens'; bytes32 constant NAMESPACE_HASH = keccak256(bytes(NAMESPACE)); - constructor(address owner, address lensHub) ERC721('', '') ImmutableOwnable(owner, lensHub) {} + constructor( + address owner, + address lensHub, + address migrator + ) ERC721('', '') ImmutableOwnable(owner, lensHub, migrator) {} function name() public pure override returns (string memory) { return string.concat(symbol(), ' Handles'); @@ -37,12 +41,12 @@ contract LensHandles is ERC721, VersionedInitializable, ImmutableOwnable { * @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 onlyOwnerOrHub returns (uint256) { + function mintHandle(address to, string calldata localName) external onlyOwnerOrHubOrMigrator returns (uint256) { bytes32 localNameHash = keccak256(bytes(localName)); bytes32 handleHash = keccak256(abi.encodePacked(localNameHash, NAMESPACE_HASH)); uint256 handleId = uint256(handleHash); _mint(to, handleId); - emit Events.HandleMinted(localName, NAMESPACE, handleId, to); + emit HandlesEvents.HandleMinted(localName, NAMESPACE, handleId, to); return handleId; } diff --git a/contracts/misc/namespaces/TokenHandleRegistry.sol b/contracts/misc/namespaces/TokenHandleRegistry.sol index 430a912..5b4b67f 100644 --- a/contracts/misc/namespaces/TokenHandleRegistry.sol +++ b/contracts/misc/namespaces/TokenHandleRegistry.sol @@ -6,7 +6,7 @@ import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol'; import {VersionedInitializable} from 'contracts/base/upgradeability/VersionedInitializable.sol'; // TODO: Move to Errors file -library Errors { +library RegistryErrors { error NotHandleOwner(); error NotTokenOwner(); error NotHandleOrTokenOwner(); @@ -26,7 +26,7 @@ struct Handle { } // TODO: Move to Events file -library Events { +library RegistryEvents { event HandleLinked(Handle handle, Token token); event HandleUnlinked(Handle handle, Token token); } @@ -53,14 +53,14 @@ contract TokenHandleRegistry is VersionedInitializable { modifier onlyHandleOwner(Handle memory handle, address transactionExecutor) { if (IERC721(handle.collection).ownerOf(handle.id) != transactionExecutor) { - revert Errors.NotHandleOwner(); + revert RegistryErrors.NotHandleOwner(); } _; } modifier onlyTokenOwner(Token memory token, address transactionExecutor) { if (IERC721(token.collection).ownerOf(token.id) != transactionExecutor) { - revert Errors.NotTokenOwner(); + revert RegistryErrors.NotTokenOwner(); } _; } @@ -75,7 +75,7 @@ contract TokenHandleRegistry is VersionedInitializable { !(IERC721(handle.collection).ownerOf(handle.id) == transactionExecutor || IERC721(token.collection).ownerOf(token.id) == transactionExecutor) ) { - revert Errors.NotHandleOrTokenOwner(); + revert RegistryErrors.NotHandleOrTokenOwner(); } _; } @@ -91,12 +91,12 @@ contract TokenHandleRegistry is VersionedInitializable { // V1->V2 Migration function function migrationLinkHandleWithToken(uint256 handleId, uint256 tokenId) external { - require(msg.sender == migrator, 'Only migrator'); + require(msg.sender == migrator || msg.sender == LENS_HUB, 'Only migrator or hub'); 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); + emit RegistryEvents.HandleLinked(handle, token); } // NOTE: Simplified interfaces for the first version - Namespace and LensHub are constants @@ -147,7 +147,7 @@ contract TokenHandleRegistry is VersionedInitializable { ) internal onlyTokenOwner(token, msg.sender) onlyHandleOwner(handle, msg.sender) { handleToToken[_handleHash(handle)] = token; tokenToHandle[_tokenHash(token)] = handle; - emit Events.HandleLinked(handle, token); + emit RegistryEvents.HandleLinked(handle, token); } function _unlinkHandleFromToken( @@ -156,7 +156,7 @@ contract TokenHandleRegistry is VersionedInitializable { ) internal onlyHandleOrTokenOwner(handle, token, msg.sender) { delete handleToToken[_handleHash(handle)]; delete tokenToHandle[_tokenHash(token)]; - emit Events.HandleUnlinked(handle, token); + emit RegistryEvents.HandleUnlinked(handle, token); } // Utility functions for mappings diff --git a/foundry-scripts/DeployUpgrade.s.sol b/foundry-scripts/DeployUpgrade.s.sol index cafd481..4508427 100644 --- a/foundry-scripts/DeployUpgrade.s.sol +++ b/foundry-scripts/DeployUpgrade.s.sol @@ -8,31 +8,65 @@ import 'contracts/LensHub.sol'; import 'contracts/FollowNFT.sol'; import 'contracts/CollectNFT.sol'; +import 'contracts/misc/migrations/ProfileMigration.sol'; +import {LensHandles} from 'contracts/misc/namespaces/LensHandles.sol'; +import {TokenHandleRegistry} from 'contracts/misc/namespaces/TokenHandleRegistry.sol'; + /** * This script will deploy the current repository implementations, using the given environment * hub proxy address. */ contract DeployUpgradeScript is Script { function run() public { - uint256 deployerKey = vm.envUint('DEPLOYER_KEY'); + string memory deployerMnemonic = vm.envString('MNEMONIC'); + uint256 deployerKey = vm.deriveKey(deployerMnemonic, 0); address deployer = vm.addr(deployerKey); address hubProxyAddr = vm.envAddress('HUB_PROXY_ADDRESS'); + address owner = deployer; + + LensHub hub = LensHub(hubProxyAddr); + address followNFTAddress = hub.getFollowNFTImpl(); + address collectNFTAddress = hub.getCollectNFTImpl(); + + uint256 deployerNonce = vm.getNonce(deployer); + + // Precompute needed addresss. + address lensHandlesAddress = computeCreateAddress(deployer, deployerNonce); + address migratorAddress = computeCreateAddress(deployer, deployerNonce + 1); + address tokenHandleRegistryAddress = computeCreateAddress(deployer, deployerNonce + 2); + // Start deployments. vm.startBroadcast(deployerKey); - // Precompute needed addresss. - address followNFTAddr = computeCreateAddress(deployer, 1); - address collectNFTAddr = computeCreateAddress(deployer, 2); + LensHandles lensHandles = new LensHandles(owner, address(hub), migratorAddress); + console.log(address(lensHandles), lensHandlesAddress); - // Deploy implementation contracts. - address hubImpl = address(new LensHub(followNFTAddr, collectNFTAddr)); - address followNFT = address(new FollowNFT(hubProxyAddr)); - address collectNFT = address(new CollectNFT(hubProxyAddr)); + ProfileMigration migrator = new ProfileMigration( + owner, + address(hub), + lensHandlesAddress, + tokenHandleRegistryAddress + ); + console.log(address(migrator), migratorAddress); - vm.writeFile('addrs', ''); - vm.writeLine('addrs', string(abi.encodePacked('hubImpl: ', vm.toString(hubImpl)))); - vm.writeLine('addrs', string(abi.encodePacked('followNFT: ', vm.toString(followNFT)))); - vm.writeLine('addrs', string(abi.encodePacked('collectNFT: ', vm.toString(collectNFT)))); + TokenHandleRegistry tokenHandleRegistry = new TokenHandleRegistry( + address(hub), + lensHandlesAddress, + migratorAddress + ); + console.log(address(tokenHandleRegistry), tokenHandleRegistryAddress); + + address hubImpl = address( + new LensHub( + followNFTAddress, + collectNFTAddress, + migratorAddress, + lensHandlesAddress, + tokenHandleRegistryAddress + ) + ); + console.log('New hub impl:', hubImpl); + vm.stopBroadcast(); } } diff --git a/hardhat.config.ts b/hardhat.config.ts index 310678a..7d08464 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -54,7 +54,7 @@ const config: HardhatUserConfig = { solidity: { compilers: [ { - version: '0.8.15', + version: '0.8.19', settings: { optimizer: { enabled: true, diff --git a/test/foundry/base/TestSetup.t.sol b/test/foundry/base/TestSetup.t.sol index b7d492f..c6d2b08 100644 --- a/test/foundry/base/TestSetup.t.sol +++ b/test/foundry/base/TestSetup.t.sol @@ -64,37 +64,8 @@ contract TestSetup is Test, ForkManagement, ArrayHelpers { Types.MirrorParams mockMirrorParams; Types.CollectParams mockCollectParams; - function isEnvSet(string memory key) internal returns (bool) { - try vm.envString(key) { - return true; - } catch { - return false; - } - } - constructor() { - // TODO: Replace with envOr when it's released - forkEnv = isEnvSet('TESTING_FORK') ? vm.envString('TESTING_FORK') : ''; - if (bytes(forkEnv).length > 0) { - fork = true; - console.log('\n\n Testing using %s fork', forkEnv); - loadJson(); - - network = getNetwork(); - - if (isEnvSet('FORK_BLOCK')) { - forkBlockNumber = vm.envUint('FORK_BLOCK'); - vm.createSelectFork(network, forkBlockNumber); - console.log('Fork Block number (FIXED BLOCK):', forkBlockNumber); - } else { - vm.createSelectFork(network); - forkBlockNumber = block.number; - console.log('Fork Block number:', forkBlockNumber); - } - - checkNetworkParams(); - loadBaseAddresses(forkEnv); } else { deployBaseContracts(); @@ -111,12 +82,6 @@ contract TestSetup is Test, ForkManagement, ArrayHelpers { ///////////////////////////////////////// End governance actions. } - // TODO: Replace with forge-std/StdJson.sol::keyExists(...) when/if this PR is approved: - // https://github.com/foundry-rs/forge-std/pull/226 - function keyExists(string memory key) internal returns (bool) { - return json.parseRaw(key).length > 0; - } - function loadBaseAddresses(string memory targetEnv) internal virtual { bytes32 PROXY_IMPLEMENTATION_STORAGE_SLOT = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1); diff --git a/test/foundry/helpers/ForkManagement.sol b/test/foundry/helpers/ForkManagement.sol index 8c7bd89..6cb9220 100644 --- a/test/foundry/helpers/ForkManagement.sol +++ b/test/foundry/helpers/ForkManagement.sol @@ -17,6 +17,47 @@ contract ForkManagement is Script { _; } + // TODO: Move somewhere else + function isEnvSet(string memory key) internal returns (bool) { + try vm.envString(key) { + return true; + } catch { + return false; + } + } + + // TODO: Move somewhere else + // TODO: Replace with forge-std/StdJson.sol::keyExists(...) when/if this PR is approved: + // https://github.com/foundry-rs/forge-std/pull/226 + function keyExists(string memory key) internal returns (bool) { + return json.parseRaw(key).length > 0; + } + + constructor() { + // TODO: Replace with envOr when it's released + forkEnv = isEnvSet('TESTING_FORK') ? vm.envString('TESTING_FORK') : ''; + + if (bytes(forkEnv).length > 0) { + fork = true; + console.log('\n\n Testing using %s fork', forkEnv); + loadJson(); + + network = getNetwork(); + + if (isEnvSet('FORK_BLOCK')) { + forkBlockNumber = vm.envUint('FORK_BLOCK'); + vm.createSelectFork(network, forkBlockNumber); + console.log('Fork Block number (FIXED BLOCK):', forkBlockNumber); + } else { + vm.createSelectFork(network); + forkBlockNumber = block.number; + console.log('Fork Block number:', forkBlockNumber); + } + + checkNetworkParams(); + } + } + function loadJson() internal { string memory root = vm.projectRoot(); string memory path = string.concat(root, '/addresses.json'); diff --git a/test/foundry/migrations/Migrations.t.sol b/test/foundry/migrations/Migrations.t.sol new file mode 100644 index 0000000..578729e --- /dev/null +++ b/test/foundry/migrations/Migrations.t.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import 'forge-std/Test.sol'; +import {ForkManagement} from 'test/foundry/helpers/ForkManagement.sol'; +import {CollectNFT} from 'contracts/CollectNFT.sol'; +import {LensHub} from 'contracts/LensHub.sol'; +import {FollowNFT} from 'contracts/FollowNFT.sol'; +import {TransparentUpgradeableProxy} from 'contracts/base/upgradeability/TransparentUpgradeableProxy.sol'; +import {ModuleGlobals} from 'contracts/misc/ModuleGlobals.sol'; +import 'contracts/misc/migrations/ProfileMigration.sol'; +import {LensHandles} from 'contracts/misc/namespaces/LensHandles.sol'; +import {TokenHandleRegistry} from 'contracts/misc/namespaces/TokenHandleRegistry.sol'; +import {Types} from 'contracts/libraries/constants/Types.sol'; + +contract MigrationsTest is Test, ForkManagement { + using stdJson for string; + + uint256 internal constant LENS_PROTOCOL_PROFILE_ID = 1; + + bytes32 constant PROXY_IMPLEMENTATION_STORAGE_SLOT = + bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1); + bytes32 constant ADMIN_SLOT = bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1); + + address owner = address(0x087E4); + address deployer = address(1); + address governance; + address treasury; + address hubProxyAddr; + address proxyAdmin; + + ProfileMigration migrator; + LensHandles lensHandles; + TokenHandleRegistry tokenHandleRegistry; + + CollectNFT collectNFT; + FollowNFT followNFT; + LensHub hubImpl; + TransparentUpgradeableProxy hubAsProxy; + LensHub hub; + ModuleGlobals moduleGlobals; + + function loadBaseAddresses(string memory targetEnv) internal virtual { + console.log('targetEnv:', targetEnv); + + hubProxyAddr = json.readAddress(string(abi.encodePacked('.', targetEnv, '.LensHubProxy'))); + console.log('hubProxyAddr:', hubProxyAddr); + + hub = LensHub(hubProxyAddr); + + console.log('Hub:', address(hub)); + + address followNFTAddr = hub.getFollowNFTImpl(); + address collectNFTAddr = hub.getCollectNFTImpl(); + + address hubImplAddr = address(uint160(uint256(vm.load(hubProxyAddr, PROXY_IMPLEMENTATION_STORAGE_SLOT)))); + console.log('Found hubImplAddr:', hubImplAddr); + + proxyAdmin = address(uint160(uint256(vm.load(hubProxyAddr, ADMIN_SLOT)))); + + followNFT = FollowNFT(followNFTAddr); + collectNFT = CollectNFT(collectNFTAddr); + hubAsProxy = TransparentUpgradeableProxy(payable(address(hub))); + moduleGlobals = ModuleGlobals(json.readAddress(string(abi.encodePacked('.', targetEnv, '.ModuleGlobals')))); + + governance = hub.getGovernance(); + } + + function setUp() public onlyFork { + loadBaseAddresses(forkEnv); + + // Precompute needed addresss. + address lensHandlesAddress = computeCreateAddress(deployer, 0); + address migratorAddress = computeCreateAddress(deployer, 1); + address tokenHandleRegistryAddress = computeCreateAddress(deployer, 2); + + vm.startPrank(deployer); + + lensHandles = new LensHandles(owner, address(hub), migratorAddress); + assertEq(address(lensHandles), lensHandlesAddress); + + migrator = new ProfileMigration(owner, address(hub), lensHandlesAddress, tokenHandleRegistryAddress); + assertEq(address(migrator), migratorAddress); + + tokenHandleRegistry = new TokenHandleRegistry(address(hub), lensHandlesAddress, migratorAddress); + assertEq(address(tokenHandleRegistry), tokenHandleRegistryAddress); + + hubImpl = new LensHub( + address(followNFT), + address(collectNFT), + migratorAddress, + lensHandlesAddress, + tokenHandleRegistryAddress + ); + vm.stopPrank(); + + vm.prank(proxyAdmin); + hubAsProxy.upgradeTo(address(hubImpl)); + } + + function testMigrationsPublic() public onlyFork { + uint256[] memory profileIds = new uint256[](10); + for (uint256 i = 0; i < 10; i++) { + profileIds[i] = i + 1; + } + hub.batchMigrateProfiles(profileIds); + } + + function testMigrationsByOwner() public onlyFork { + ProfileMigrationData[] memory profileMigrationDatas = new ProfileMigrationData[](10); + + for (uint256 i = 0; i < 10; i++) { + uint256 profileId = i + 1; + string memory handleWithLens = hub.getProfile(profileId).handleDeprecated; + string memory handle = hub.getProfile(profileId).handleDeprecated; + + 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 + } + } + + profileMigrationDatas[i] = ProfileMigrationData({ + profileId: profileId, + profileDestination: hub.ownerOf(profileId), + handle: handle, + handleHash: keccak256(bytes(handleWithLens)) + }); + } + + vm.prank(owner); + migrator.batchMigrateProfiles(profileMigrationDatas); + } +}