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.
- package/contracts/FollowNFT.sol +507 -0
- package/contracts/LensHub.sol +590 -0
- package/contracts/base/LensGovernable.sol +114 -0
- package/contracts/base/LensHubEventHooks.sol +42 -0
- package/contracts/base/LensHubStorage.sol +69 -0
- package/contracts/base/LensImplGetters.sol +32 -0
- package/contracts/base/LensProfiles.sol +162 -0
- package/contracts/base/LensVersion.sol +36 -0
- package/contracts/base/upgradeability/FollowNFTProxy.sol +21 -0
- package/contracts/interfaces/IFollowTokenURI.sol +11 -0
- package/contracts/interfaces/ILegacyCollectModule.sol +45 -0
- package/contracts/interfaces/ILegacyCollectNFT.sol +46 -0
- package/contracts/interfaces/ILegacyFollowModule.sol +82 -0
- package/contracts/interfaces/IProfileTokenURI.sol +7 -0
- package/contracts/interfaces/ITokenHandleRegistry.sol +90 -0
- package/contracts/libraries/ActionLib.sol +69 -0
- package/contracts/libraries/FollowLib.sol +175 -0
- package/contracts/libraries/GovernanceLib.sol +111 -0
- package/contracts/libraries/LegacyCollectLib.sol +176 -0
- package/contracts/libraries/ProfileLib.sol +284 -0
- package/contracts/libraries/PublicationLib.sol +567 -0
- package/contracts/libraries/ValidationLib.sol +228 -0
- package/dist/index.d.ts +5 -0
- package/dist/{src/index.js → index.js} +4 -4
- package/dist/verified-modules/follow-modules.d.ts +12 -0
- package/dist/verified-modules/follow-modules.js +15 -0
- package/dist/verified-modules/open-actions.d.ts +42 -0
- package/dist/verified-modules/open-actions.js +45 -0
- package/dist/verified-modules/reference-modules.d.ts +17 -0
- package/dist/verified-modules/reference-modules.js +20 -0
- package/package.json +1 -1
- package/dist/src/index.d.ts +0 -5
- /package/dist/{deployments/lens-contracts.d.ts → lens-contracts.d.ts} +0 -0
- /package/dist/{deployments/lens-contracts.js → lens-contracts.js} +0 -0
- /package/dist/{src/lenshub-abi.d.ts → lenshub-abi.d.ts} +0 -0
- /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
|
+
}
|