lens-modules 1.0.13 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/contracts/FollowNFT.sol +507 -0
  2. package/contracts/LensHub.sol +590 -0
  3. package/contracts/base/LensGovernable.sol +114 -0
  4. package/contracts/base/LensHubEventHooks.sol +42 -0
  5. package/contracts/base/LensHubStorage.sol +69 -0
  6. package/contracts/base/LensImplGetters.sol +32 -0
  7. package/contracts/base/LensProfiles.sol +162 -0
  8. package/contracts/base/LensVersion.sol +36 -0
  9. package/contracts/base/upgradeability/FollowNFTProxy.sol +21 -0
  10. package/contracts/interfaces/IFollowTokenURI.sol +11 -0
  11. package/contracts/interfaces/ILegacyCollectModule.sol +45 -0
  12. package/contracts/interfaces/ILegacyCollectNFT.sol +46 -0
  13. package/contracts/interfaces/ILegacyFollowModule.sol +82 -0
  14. package/contracts/interfaces/IProfileTokenURI.sol +7 -0
  15. package/contracts/interfaces/ITokenHandleRegistry.sol +90 -0
  16. package/contracts/libraries/ActionLib.sol +69 -0
  17. package/contracts/libraries/FollowLib.sol +175 -0
  18. package/contracts/libraries/GovernanceLib.sol +111 -0
  19. package/contracts/libraries/LegacyCollectLib.sol +176 -0
  20. package/contracts/libraries/ProfileLib.sol +284 -0
  21. package/contracts/libraries/PublicationLib.sol +567 -0
  22. package/contracts/libraries/ValidationLib.sol +228 -0
  23. package/dist/index.d.ts +5 -0
  24. package/dist/{src/index.js → index.js} +4 -4
  25. package/dist/verified-modules/follow-modules.d.ts +12 -0
  26. package/dist/verified-modules/follow-modules.js +15 -0
  27. package/dist/verified-modules/open-actions.d.ts +42 -0
  28. package/dist/verified-modules/open-actions.js +45 -0
  29. package/dist/verified-modules/reference-modules.d.ts +17 -0
  30. package/dist/verified-modules/reference-modules.js +20 -0
  31. package/package.json +1 -1
  32. package/dist/src/index.d.ts +0 -5
  33. /package/dist/{deployments/lens-contracts.d.ts → lens-contracts.d.ts} +0 -0
  34. /package/dist/{deployments/lens-contracts.js → lens-contracts.js} +0 -0
  35. /package/dist/{src/lenshub-abi.d.ts → lenshub-abi.d.ts} +0 -0
  36. /package/dist/{src/lenshub-abi.js → lenshub-abi.js} +0 -0
@@ -0,0 +1,507 @@
1
+ // SPDX-License-Identifier: MIT
2
+
3
+ pragma solidity ^0.8.15;
4
+
5
+ import {Types} from './libraries/constants/Types.sol';
6
+ import {ERC2981CollectionRoyalties} from './base/ERC2981CollectionRoyalties.sol';
7
+ import {Errors} from './libraries/constants/Errors.sol';
8
+ import {HubRestricted} from './base/HubRestricted.sol';
9
+ import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
10
+ import {IERC721Timestamped} from './interfaces/IERC721Timestamped.sol';
11
+ import {IFollowNFT} from './interfaces/IFollowNFT.sol';
12
+ import {ILensHub} from './interfaces/ILensHub.sol';
13
+ import {LensBaseERC721} from './base/LensBaseERC721.sol';
14
+ import {Strings} from '@openzeppelin/contracts/utils/Strings.sol';
15
+ import {StorageLib} from './libraries/StorageLib.sol';
16
+ import {Types} from './libraries/constants/Types.sol';
17
+ import {IFollowTokenURI} from './interfaces/IFollowTokenURI.sol';
18
+
19
+ /**
20
+ * @custom:upgradeable Beacon proxy. The beacon, responsible for returning the implementation address, is the LensHub.
21
+ */
22
+ contract FollowNFT is HubRestricted, LensBaseERC721, ERC2981CollectionRoyalties, IFollowNFT {
23
+ using Strings for uint256;
24
+
25
+ string constant FOLLOW_NFT_NAME_SUFFIX = '-Follower';
26
+ string constant FOLLOW_NFT_SYMBOL_SUFFIX = '-Fl';
27
+
28
+ uint256[5] ___DEPRECATED_SLOTS; // Deprecated slots, previously used for delegations.
29
+ uint256 internal _followedProfileId;
30
+
31
+ // Old uint256 `_lastFollowTokenId` slot splitted into two uint128s to include `_followerCount`.
32
+ uint128 internal _lastFollowTokenId;
33
+ // `_followerCount` will not be decreased when a follower profile is burned, making the counter not fully accurate.
34
+ // New variable added in V2 in the same slot, lower-ordered to not conflict with previous storage layout.
35
+ uint128 internal _followerCount;
36
+
37
+ bool private _initialized;
38
+
39
+ // Introduced in v2
40
+ mapping(uint256 => Types.FollowData) internal _followDataByFollowTokenId;
41
+ mapping(uint256 => uint256) internal _followTokenIdByFollowerProfileId;
42
+ mapping(uint256 => uint256) internal _followApprovalByFollowTokenId;
43
+ uint256 internal _royaltiesInBasisPoints;
44
+
45
+ event FollowApproval(uint256 indexed followerProfileId, uint256 indexed followTokenId);
46
+
47
+ modifier whenNotPaused() {
48
+ if (ILensHub(HUB).getState() == Types.ProtocolState.Paused) {
49
+ revert Errors.Paused();
50
+ }
51
+ _;
52
+ }
53
+
54
+ constructor(address hub) HubRestricted(hub) {
55
+ _initialized = true;
56
+ }
57
+
58
+ /// @inheritdoc IFollowNFT
59
+ function initialize(uint256 profileId) external override {
60
+ // This is called right after deployment by the LensHub, so we can skip the onlyHub check.
61
+ if (_initialized) {
62
+ revert Errors.Initialized();
63
+ }
64
+ _initialized = true;
65
+ _followedProfileId = profileId;
66
+ _setRoyalty(1000); // 10% of royalties
67
+ }
68
+
69
+ /// @inheritdoc IFollowNFT
70
+ function follow(
71
+ uint256 followerProfileId,
72
+ address transactionExecutor,
73
+ uint256 followTokenId
74
+ ) external override onlyHub returns (uint256) {
75
+ if (_followTokenIdByFollowerProfileId[followerProfileId] != 0) {
76
+ revert AlreadyFollowing();
77
+ }
78
+
79
+ if (followTokenId == 0) {
80
+ // Fresh follow.
81
+ return _followMintingNewToken(followerProfileId);
82
+ }
83
+
84
+ address followTokenOwner = _unsafeOwnerOf(followTokenId);
85
+ if (followTokenOwner != address(0)) {
86
+ // Provided follow token is wrapped.
87
+ return
88
+ _followWithWrappedToken({
89
+ followerProfileId: followerProfileId,
90
+ transactionExecutor: transactionExecutor,
91
+ followTokenId: followTokenId
92
+ });
93
+ }
94
+
95
+ uint256 currentFollowerProfileId = _followDataByFollowTokenId[followTokenId].followerProfileId;
96
+ if (currentFollowerProfileId != 0) {
97
+ // Provided follow token is unwrapped.
98
+ // It has a follower profile set already, it can only be used to follow if that profile was burnt.
99
+ return
100
+ _followWithUnwrappedTokenFromBurnedProfile({
101
+ followerProfileId: followerProfileId,
102
+ followTokenId: followTokenId,
103
+ currentFollowerProfileId: currentFollowerProfileId,
104
+ transactionExecutor: transactionExecutor
105
+ });
106
+ }
107
+
108
+ // Provided follow token does not exist anymore, it can only be used if the profile attempting to follow is
109
+ // allowed to recover it.
110
+ return _followByRecoveringToken({followerProfileId: followerProfileId, followTokenId: followTokenId});
111
+ }
112
+
113
+ /// @inheritdoc IFollowNFT
114
+ function unfollow(uint256 unfollowerProfileId) external override onlyHub {
115
+ uint256 followTokenId = _followTokenIdByFollowerProfileId[unfollowerProfileId];
116
+ if (followTokenId == 0) {
117
+ revert NotFollowing();
118
+ }
119
+ address followTokenOwner = _unsafeOwnerOf(followTokenId);
120
+ // LensHub already validated that this action can only be performed by the unfollower profile's owner or one of
121
+ // his approved delegated executors.
122
+ _unfollow({unfollower: unfollowerProfileId, followTokenId: followTokenId});
123
+ if (followTokenOwner == address(0)) {
124
+ // Follow token was unwrapped, allowing recovery.
125
+ _followDataByFollowTokenId[followTokenId].profileIdAllowedToRecover = unfollowerProfileId;
126
+ }
127
+ }
128
+
129
+ /// @inheritdoc IFollowNFT
130
+ function removeFollower(uint256 followTokenId) external override whenNotPaused {
131
+ if (_isApprovedOrOwner(msg.sender, followTokenId)) {
132
+ _unfollowIfHasFollower(followTokenId, msg.sender);
133
+ } else {
134
+ revert DoesNotHavePermissions();
135
+ }
136
+ }
137
+
138
+ /// @inheritdoc IFollowNFT
139
+ function approveFollow(uint256 followerProfileId, uint256 followTokenId) external override {
140
+ if (!IERC721Timestamped(HUB).exists(followerProfileId)) {
141
+ revert Errors.TokenDoesNotExist();
142
+ }
143
+ // `followTokenId` allowed to be zero as a way to clear the approval.
144
+ if (followTokenId != 0 && _unsafeOwnerOf(followTokenId) == address(0)) {
145
+ revert OnlyWrappedFollowTokens();
146
+ }
147
+ if (_isApprovedOrOwner(msg.sender, followTokenId)) {
148
+ _approveFollow(followerProfileId, followTokenId);
149
+ } else {
150
+ revert DoesNotHavePermissions();
151
+ }
152
+ }
153
+
154
+ /// @inheritdoc IFollowNFT
155
+ function wrap(uint256 followTokenId, address wrappedTokenReceiver) external override whenNotPaused {
156
+ if (wrappedTokenReceiver == address(0)) {
157
+ revert Errors.InvalidParameter();
158
+ }
159
+ _wrap(followTokenId, wrappedTokenReceiver);
160
+ }
161
+
162
+ /// @inheritdoc IFollowNFT
163
+ function wrap(uint256 followTokenId) external override whenNotPaused {
164
+ _wrap(followTokenId, address(0));
165
+ }
166
+
167
+ function _wrap(uint256 followTokenId, address wrappedTokenReceiver) internal {
168
+ if (_isFollowTokenWrapped(followTokenId)) {
169
+ revert AlreadyWrapped();
170
+ }
171
+ uint256 followerProfileId = _followDataByFollowTokenId[followTokenId].followerProfileId;
172
+ if (followerProfileId == 0) {
173
+ followerProfileId = _followDataByFollowTokenId[followTokenId].profileIdAllowedToRecover;
174
+ if (followerProfileId == 0) {
175
+ revert FollowTokenDoesNotExist();
176
+ }
177
+ delete _followDataByFollowTokenId[followTokenId].profileIdAllowedToRecover;
178
+ }
179
+ address followerProfileOwner = IERC721(HUB).ownerOf(followerProfileId);
180
+ if (msg.sender != followerProfileOwner) {
181
+ revert DoesNotHavePermissions();
182
+ }
183
+ _mint(wrappedTokenReceiver == address(0) ? followerProfileOwner : wrappedTokenReceiver, followTokenId);
184
+ }
185
+
186
+ /// @inheritdoc IFollowNFT
187
+ function unwrap(uint256 followTokenId) external override whenNotPaused {
188
+ if (_followDataByFollowTokenId[followTokenId].followerProfileId == 0) {
189
+ revert NotFollowing();
190
+ }
191
+ super.burn(followTokenId);
192
+ }
193
+
194
+ /// @inheritdoc IFollowNFT
195
+ function processBlock(uint256 followerProfileId) external override onlyHub returns (bool) {
196
+ bool hasUnfollowed;
197
+ uint256 followTokenId = _followTokenIdByFollowerProfileId[followerProfileId];
198
+ if (followTokenId != 0) {
199
+ if (!_isFollowTokenWrapped(followTokenId)) {
200
+ // Wrap it first, so the user stops following but does not lose the token when being blocked.
201
+ _mint(IERC721(HUB).ownerOf(followerProfileId), followTokenId);
202
+ }
203
+ _unfollow(followerProfileId, followTokenId);
204
+ hasUnfollowed = true;
205
+ }
206
+ return hasUnfollowed;
207
+ }
208
+
209
+ /// @inheritdoc IFollowNFT
210
+ function getFollowerProfileId(uint256 followTokenId) external view override returns (uint256) {
211
+ return _followDataByFollowTokenId[followTokenId].followerProfileId;
212
+ }
213
+
214
+ /// @inheritdoc IFollowNFT
215
+ function isFollowing(uint256 followerProfileId) external view override returns (bool) {
216
+ return _followTokenIdByFollowerProfileId[followerProfileId] != 0;
217
+ }
218
+
219
+ /// @inheritdoc IFollowNFT
220
+ function getFollowTokenId(uint256 followerProfileId) external view override returns (uint256) {
221
+ return _followTokenIdByFollowerProfileId[followerProfileId];
222
+ }
223
+
224
+ /// @inheritdoc IFollowNFT
225
+ function getOriginalFollowTimestamp(uint256 followTokenId) external view override returns (uint256) {
226
+ return _followDataByFollowTokenId[followTokenId].originalFollowTimestamp;
227
+ }
228
+
229
+ /// @inheritdoc IFollowNFT
230
+ function getFollowTimestamp(uint256 followTokenId) external view override returns (uint256) {
231
+ return _followDataByFollowTokenId[followTokenId].followTimestamp;
232
+ }
233
+
234
+ /// @inheritdoc IFollowNFT
235
+ function getProfileIdAllowedToRecover(uint256 followTokenId) external view override returns (uint256) {
236
+ return _followDataByFollowTokenId[followTokenId].profileIdAllowedToRecover;
237
+ }
238
+
239
+ /// @inheritdoc IFollowNFT
240
+ function getFollowData(uint256 followTokenId) external view override returns (Types.FollowData memory) {
241
+ return _followDataByFollowTokenId[followTokenId];
242
+ }
243
+
244
+ /// @inheritdoc IFollowNFT
245
+ function getFollowApproved(uint256 followTokenId) external view override returns (uint256) {
246
+ return _followApprovalByFollowTokenId[followTokenId];
247
+ }
248
+
249
+ /// @inheritdoc IFollowNFT
250
+ function getFollowerCount() external view override returns (uint256) {
251
+ return _followerCount;
252
+ }
253
+
254
+ function burn(uint256 followTokenId) public override whenNotPaused {
255
+ _unfollowIfHasFollower(followTokenId, msg.sender);
256
+ super.burn(followTokenId);
257
+ }
258
+
259
+ /**
260
+ * @dev See {IERC165-supportsInterface}.
261
+ */
262
+ function supportsInterface(
263
+ bytes4 interfaceId
264
+ ) public view virtual override(LensBaseERC721, ERC2981CollectionRoyalties) returns (bool) {
265
+ return
266
+ LensBaseERC721.supportsInterface(interfaceId) || ERC2981CollectionRoyalties.supportsInterface(interfaceId);
267
+ }
268
+
269
+ function name() public view override returns (string memory) {
270
+ return string(abi.encodePacked(_followedProfileId.toString(), FOLLOW_NFT_NAME_SUFFIX));
271
+ }
272
+
273
+ function symbol() public view override returns (string memory) {
274
+ return string(abi.encodePacked(_followedProfileId.toString(), FOLLOW_NFT_SYMBOL_SUFFIX));
275
+ }
276
+
277
+ /**
278
+ * @dev This returns the follow NFT URI fetched from the hub.
279
+ */
280
+ function tokenURI(uint256 followTokenId) public view override returns (string memory) {
281
+ if (!_exists(followTokenId)) {
282
+ revert Errors.TokenDoesNotExist();
283
+ }
284
+ return
285
+ IFollowTokenURI(ILensHub(HUB).getFollowTokenURIContract()).getTokenURI(
286
+ followTokenId,
287
+ _followedProfileId,
288
+ _followDataByFollowTokenId[followTokenId].originalFollowTimestamp
289
+ );
290
+ }
291
+
292
+ function _followMintingNewToken(uint256 followerProfileId) internal returns (uint256) {
293
+ uint256 followTokenIdAssigned;
294
+ unchecked {
295
+ followTokenIdAssigned = ++_lastFollowTokenId;
296
+ _followerCount++;
297
+ }
298
+ _baseFollow({
299
+ followerProfileId: followerProfileId,
300
+ followTokenId: followTokenIdAssigned,
301
+ isOriginalFollow: true
302
+ });
303
+ return followTokenIdAssigned;
304
+ }
305
+
306
+ function _followWithWrappedToken(
307
+ uint256 followerProfileId,
308
+ address transactionExecutor,
309
+ uint256 followTokenId
310
+ ) internal returns (uint256) {
311
+ bool isFollowApproved = _followApprovalByFollowTokenId[followTokenId] == followerProfileId;
312
+ address followerProfileOwner = IERC721(HUB).ownerOf(followerProfileId);
313
+ if (
314
+ !isFollowApproved &&
315
+ !_isApprovedOrOwner(followerProfileOwner, followTokenId) &&
316
+ !_isApprovedOrOwner(transactionExecutor, followTokenId)
317
+ ) {
318
+ revert DoesNotHavePermissions();
319
+ }
320
+ // The transactionExecutor is allowed to write the follower in that wrapped token.
321
+ if (isFollowApproved) {
322
+ // The `_followApprovalByFollowTokenId` was used, and now it needs to be cleared.
323
+ _approveFollow(0, followTokenId);
324
+ }
325
+ _replaceFollower({
326
+ currentFollowerProfileId: _followDataByFollowTokenId[followTokenId].followerProfileId,
327
+ newFollowerProfileId: followerProfileId,
328
+ followTokenId: followTokenId,
329
+ transactionExecutor: transactionExecutor
330
+ });
331
+ return followTokenId;
332
+ }
333
+
334
+ function _followWithUnwrappedTokenFromBurnedProfile(
335
+ uint256 followerProfileId,
336
+ uint256 followTokenId,
337
+ uint256 currentFollowerProfileId,
338
+ address transactionExecutor
339
+ ) internal returns (uint256) {
340
+ if (IERC721Timestamped(HUB).exists(currentFollowerProfileId)) {
341
+ revert DoesNotHavePermissions();
342
+ }
343
+ _replaceFollower({
344
+ currentFollowerProfileId: currentFollowerProfileId,
345
+ newFollowerProfileId: followerProfileId,
346
+ followTokenId: followTokenId,
347
+ transactionExecutor: transactionExecutor
348
+ });
349
+ return followTokenId;
350
+ }
351
+
352
+ function _followByRecoveringToken(uint256 followerProfileId, uint256 followTokenId) internal returns (uint256) {
353
+ if (_followDataByFollowTokenId[followTokenId].profileIdAllowedToRecover != followerProfileId) {
354
+ revert FollowTokenDoesNotExist();
355
+ }
356
+ unchecked {
357
+ _followerCount++;
358
+ }
359
+ _baseFollow({followerProfileId: followerProfileId, followTokenId: followTokenId, isOriginalFollow: false});
360
+ return followTokenId;
361
+ }
362
+
363
+ function _replaceFollower(
364
+ uint256 currentFollowerProfileId,
365
+ uint256 newFollowerProfileId,
366
+ uint256 followTokenId,
367
+ address transactionExecutor
368
+ ) internal {
369
+ if (currentFollowerProfileId != 0) {
370
+ // As it has a follower, unfollow first, removing the current follower.
371
+ delete _followTokenIdByFollowerProfileId[currentFollowerProfileId];
372
+ ILensHub(HUB).emitUnfollowedEvent(currentFollowerProfileId, _followedProfileId, transactionExecutor);
373
+ } else {
374
+ unchecked {
375
+ _followerCount++;
376
+ }
377
+ }
378
+ // Perform the follow, setting a new follower.
379
+ _baseFollow({followerProfileId: newFollowerProfileId, followTokenId: followTokenId, isOriginalFollow: false});
380
+ }
381
+
382
+ function _baseFollow(uint256 followerProfileId, uint256 followTokenId, bool isOriginalFollow) internal {
383
+ _followTokenIdByFollowerProfileId[followerProfileId] = followTokenId;
384
+ _followDataByFollowTokenId[followTokenId].followerProfileId = uint160(followerProfileId);
385
+ _followDataByFollowTokenId[followTokenId].followTimestamp = uint48(block.timestamp);
386
+ delete _followDataByFollowTokenId[followTokenId].profileIdAllowedToRecover;
387
+ if (isOriginalFollow) {
388
+ _followDataByFollowTokenId[followTokenId].originalFollowTimestamp = uint48(block.timestamp);
389
+ } else {
390
+ // Migration code.
391
+ // If the follow token was minted before the originalFollowTimestamp was introduced, it will be 0.
392
+ // In that case, we need to fetch the mint timestamp from the token data.
393
+ if (_followDataByFollowTokenId[followTokenId].originalFollowTimestamp == 0) {
394
+ uint48 mintTimestamp = uint48(StorageLib.getTokenData(followTokenId).mintTimestamp);
395
+ _followDataByFollowTokenId[followTokenId].originalFollowTimestamp = mintTimestamp;
396
+ }
397
+ }
398
+ }
399
+
400
+ function _unfollowIfHasFollower(uint256 followTokenId, address transactionExecutor) internal {
401
+ uint256 followerProfileId = _followDataByFollowTokenId[followTokenId].followerProfileId;
402
+ if (followerProfileId != 0) {
403
+ _unfollow(followerProfileId, followTokenId);
404
+ ILensHub(HUB).emitUnfollowedEvent(followerProfileId, _followedProfileId, transactionExecutor);
405
+ }
406
+ }
407
+
408
+ function _unfollow(uint256 unfollower, uint256 followTokenId) internal {
409
+ unchecked {
410
+ // This is safe, as this line can only be reached if the unfollowed profile is being followed by the
411
+ // unfollower profile, so _followerCount is guaranteed to be greater than zero.
412
+ _followerCount--;
413
+ }
414
+ delete _followTokenIdByFollowerProfileId[unfollower];
415
+ delete _followDataByFollowTokenId[followTokenId].followerProfileId;
416
+ delete _followDataByFollowTokenId[followTokenId].followTimestamp;
417
+ delete _followDataByFollowTokenId[followTokenId].profileIdAllowedToRecover;
418
+ }
419
+
420
+ function _approveFollow(uint256 approvedProfileId, uint256 followTokenId) internal {
421
+ _followApprovalByFollowTokenId[followTokenId] = approvedProfileId;
422
+ emit FollowApproval(approvedProfileId, followTokenId);
423
+ }
424
+
425
+ /**
426
+ * @dev Upon transfers, we clear follow approvals and emit the transfer event in the hub.
427
+ */
428
+ function _beforeTokenTransfer(address from, address to, uint256 followTokenId) internal override whenNotPaused {
429
+ if (from != address(0)) {
430
+ // It is cleared on unwrappings and transfers, and it can not be set on unwrapped tokens.
431
+ // As a consequence, there is no need to clear it on wrappings.
432
+ _approveFollow(0, followTokenId);
433
+ }
434
+ super._beforeTokenTransfer(from, to, followTokenId);
435
+ }
436
+
437
+ function _getReceiver(uint256 /* followTokenId */) internal view override returns (address) {
438
+ if (!ILensHub(HUB).exists(_followedProfileId)) {
439
+ return address(0);
440
+ }
441
+ return IERC721(HUB).ownerOf(_followedProfileId);
442
+ }
443
+
444
+ function _beforeRoyaltiesSet(uint256 /* royaltiesInBasisPoints */) internal view override {
445
+ if (IERC721(HUB).ownerOf(_followedProfileId) != msg.sender) {
446
+ revert Errors.NotProfileOwner();
447
+ }
448
+ }
449
+
450
+ function _isFollowTokenWrapped(uint256 followTokenId) internal view returns (bool) {
451
+ return _exists(followTokenId);
452
+ }
453
+
454
+ function _getRoyaltiesInBasisPointsSlot() internal pure override returns (uint256) {
455
+ uint256 slot;
456
+ assembly {
457
+ slot := _royaltiesInBasisPoints.slot
458
+ }
459
+ return slot;
460
+ }
461
+
462
+ //////////////////
463
+ /// Migrations ///
464
+ //////////////////
465
+
466
+ // This function shouldn't fail under no circumstances, except if wrong parameters are passed.
467
+ function tryMigrate(
468
+ uint256 followerProfileId,
469
+ address followerProfileOwner,
470
+ uint256 followTokenId
471
+ ) external onlyHub returns (uint48) {
472
+ // Migrated FollowNFTs should have `originalFollowTimestamp` set
473
+ if (_followDataByFollowTokenId[followTokenId].originalFollowTimestamp != 0) {
474
+ return 0; // Already migrated
475
+ }
476
+
477
+ if (_followTokenIdByFollowerProfileId[followerProfileId] != 0) {
478
+ return 0; // Already following
479
+ }
480
+
481
+ Types.TokenData memory tokenData = StorageLib.getTokenData(followTokenId);
482
+
483
+ address followTokenOwner = tokenData.owner;
484
+
485
+ if (followTokenOwner == address(0)) {
486
+ return 0; // Doesn't exist
487
+ }
488
+
489
+ // ProfileNFT and FollowNFT should be in the same account
490
+ if (followerProfileOwner != followTokenOwner) {
491
+ return 0; // Not holding both Profile & Follow NFTs together
492
+ }
493
+
494
+ unchecked {
495
+ ++_followerCount;
496
+ }
497
+
498
+ _followTokenIdByFollowerProfileId[followerProfileId] = followTokenId;
499
+
500
+ _followDataByFollowTokenId[followTokenId].followerProfileId = uint160(followerProfileId);
501
+ _followDataByFollowTokenId[followTokenId].originalFollowTimestamp = uint48(tokenData.mintTimestamp);
502
+ _followDataByFollowTokenId[followTokenId].followTimestamp = uint48(tokenData.mintTimestamp);
503
+
504
+ super._burn(followTokenId);
505
+ return uint48(tokenData.mintTimestamp);
506
+ }
507
+ }