Skip to content

Commit 18b3f14

Browse files
author
Zer0dot
authored
Merge pull request #128 from aave/refactor/profile-follow-module
2 parents 3e41544 + 1785b77 commit 18b3f14

File tree

2 files changed

+44
-95
lines changed

2 files changed

+44
-95
lines changed

contracts/core/modules/follow/ProfileFollowModule.sol

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,40 +12,38 @@ import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
1212
* @title ProfileFollowModule
1313
* @author Lens Protocol
1414
*
15-
* @notice This follow module only allows profiles that are not already following in the current revision to follow.
15+
* @notice A Lens Profile NFT token-gated follow module with single follow per token validation.
1616
*/
1717
contract ProfileFollowModule is FollowValidatorFollowModuleBase {
18-
mapping(uint256 => mapping(uint256 => mapping(uint256 => bool)))
19-
internal _isProfileFollowingByRevisionByProfile;
20-
21-
mapping(uint256 => uint256) internal _revisionByProfile;
18+
/**
19+
* Given two profile IDs tells if the former has already been used to follow the latter.
20+
*/
21+
mapping(uint256 => mapping(uint256 => bool)) public isProfileFollowing;
2222

2323
constructor(address hub) ModuleBase(hub) {}
2424

2525
/**
2626
* @notice This follow module works on custom profile owner approvals.
2727
*
2828
* @param profileId The profile ID of the profile to initialize this module for.
29-
* @param data The arbitrary data parameter, decoded into:
30-
* uint256 revision: The revision number to be used in this module initialization.
29+
* @param data The arbitrary data parameter, which in this particular module initialization will be just ignored.
3130
*
32-
* @return bytes An abi encoded bytes parameter, which is the same as the passed data parameter.
31+
* @return bytes Empty bytes.
3332
*/
3433
function initializeFollowModule(uint256 profileId, bytes calldata data)
3534
external
3635
override
3736
onlyHub
3837
returns (bytes memory)
3938
{
40-
_revisionByProfile[profileId] = abi.decode(data, (uint256));
41-
return data;
39+
return new bytes(0);
4240
}
4341

4442
/**
4543
* @dev Processes a follow by:
46-
* 1. Validating that the follower owns the profile passed through the data param
47-
* 2. Validating that the profile that is being used to execute the follow is not already following
48-
* the given profile in the current revision
44+
* 1. Validating that the follower owns the profile passed through the data param.
45+
* 2. Validating that the profile that is being used to execute the follow was not already used for following the
46+
* given profile.
4947
*/
5048
function processFollow(
5149
address follower,
@@ -56,11 +54,10 @@ contract ProfileFollowModule is FollowValidatorFollowModuleBase {
5654
if (IERC721(HUB).ownerOf(followerProfileId) != follower) {
5755
revert Errors.NotProfileOwner();
5856
}
59-
uint256 revision = _revisionByProfile[profileId];
60-
if (_isProfileFollowingByRevisionByProfile[profileId][revision][followerProfileId]) {
57+
if (isProfileFollowing[followerProfileId][profileId]) {
6158
revert Errors.FollowInvalid();
6259
} else {
63-
_isProfileFollowingByRevisionByProfile[profileId][revision][followerProfileId] = true;
60+
isProfileFollowing[followerProfileId][profileId] = true;
6461
}
6562
}
6663

test/modules/follow/profile-follow-module.spec.ts

Lines changed: 31 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ import {
2222
} from '../../__setup.spec';
2323

2424
makeSuiteCleanRoom('Profile Follow Module', function () {
25-
let DEFAULT_INIT_DATA: BytesLike;
25+
let EMPTY_BYTES: BytesLike;
2626
let DEFAULT_FOLLOW_DATA: BytesLike;
2727

2828
before(async function () {
29-
DEFAULT_INIT_DATA = abiCoder.encode(['uint256'], [0]);
29+
EMPTY_BYTES = '0x';
3030
DEFAULT_FOLLOW_DATA = abiCoder.encode(['uint256'], [FIRST_PROFILE_ID + 1]);
3131
await expect(
3232
lensHub.connect(governance).whitelistFollowModule(profileFollowModule.address, true)
@@ -37,15 +37,9 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
3737
context('Initialization', function () {
3838
it('Initialize call should fail when sender is not the hub', async function () {
3939
await expect(
40-
profileFollowModule.initializeFollowModule(FIRST_PROFILE_ID, DEFAULT_INIT_DATA)
40+
profileFollowModule.initializeFollowModule(FIRST_PROFILE_ID, EMPTY_BYTES)
4141
).to.be.revertedWith(ERRORS.NOT_HUB);
4242
});
43-
44-
it('Initialize call should fail when data is not holding the revision number encoded', async function () {
45-
await expect(
46-
profileFollowModule.connect(lensHub.address).initializeFollowModule(FIRST_PROFILE_ID, [])
47-
).to.be.reverted;
48-
});
4943
});
5044

5145
context('Following', function () {
@@ -56,7 +50,7 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
5650
handle: MOCK_PROFILE_HANDLE,
5751
imageURI: MOCK_PROFILE_URI,
5852
followModule: profileFollowModule.address,
59-
followModuleInitData: DEFAULT_INIT_DATA,
53+
followModuleInitData: EMPTY_BYTES,
6054
followNFTURI: MOCK_FOLLOW_NFT_URI,
6155
})
6256
).to.not.be.reverted;
@@ -118,26 +112,7 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
118112
).to.be.revertedWith(ERRORS.NOT_PROFILE_OWNER);
119113
});
120114

121-
it('Follow should fail when the passed follower profile has already followed the profile in the current revision', async function () {
122-
await expect(
123-
lensHub.createProfile({
124-
to: userTwoAddress,
125-
handle: 'usertwo',
126-
imageURI: MOCK_PROFILE_URI,
127-
followModule: ZERO_ADDRESS,
128-
followModuleInitData: [],
129-
followNFTURI: MOCK_FOLLOW_NFT_URI,
130-
})
131-
).to.not.be.reverted;
132-
await expect(
133-
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
134-
).to.not.be.reverted;
135-
await expect(
136-
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
137-
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
138-
});
139-
140-
it('Follow should fail when switching to an old revision where the passed follower profile has already followed the profile', async function () {
115+
it('Follow should fail when the passed follower profile has already followed the profile', async function () {
141116
await expect(
142117
lensHub.createProfile({
143118
to: userTwoAddress,
@@ -151,27 +126,16 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
151126
await expect(
152127
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
153128
).to.not.be.reverted;
154-
155-
// Update the revision
156-
const data = abiCoder.encode(['uint256'], [1]);
157-
await expect(
158-
lensHub.setFollowModule(FIRST_PROFILE_ID, profileFollowModule.address, data)
159-
).to.not.be.reverted;
160-
// We check that profile can be followed again but through callStatic to avoid state-changes
161-
await expect(
162-
lensHub.connect(userTwo).callStatic.follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
163-
).to.not.be.reverted;
164-
165-
// Return the revision to the original, follow should be invalid
166-
await expect(
167-
lensHub.setFollowModule(FIRST_PROFILE_ID, profileFollowModule.address, DEFAULT_INIT_DATA)
168-
).to.not.be.reverted;
129+
const followerProfileId = FIRST_PROFILE_ID + 1;
130+
expect(
131+
await profileFollowModule.isProfileFollowing(followerProfileId, FIRST_PROFILE_ID)
132+
).to.be.true;
169133
await expect(
170134
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
171135
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
172136
});
173137

174-
it('Follow should fail when the passed follower profile has already followed the profile in the current revision even after the profile nft has been transfered', async function () {
138+
it('Follow should fail when the passed follower profile has already followed the profile even after the profile nft has been transfered', async function () {
175139
await expect(
176140
lensHub.createProfile({
177141
to: userTwoAddress,
@@ -185,10 +149,18 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
185149
await expect(
186150
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
187151
).to.not.be.reverted;
152+
const followerProfileId = FIRST_PROFILE_ID + 1;
153+
expect(
154+
await profileFollowModule.isProfileFollowing(followerProfileId, FIRST_PROFILE_ID)
155+
).to.be.true;
188156

189157
await expect(
190158
lensHub.transferFrom(userAddress, userThreeAddress, FIRST_PROFILE_ID)
191159
).to.not.be.reverted;
160+
expect(
161+
await profileFollowModule.isProfileFollowing(followerProfileId, FIRST_PROFILE_ID)
162+
).to.be.true;
163+
192164
await expect(
193165
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
194166
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
@@ -198,12 +170,12 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
198170

199171
context('Scenarios', function () {
200172
context('Initialization', function () {
201-
it('Initialize call should succeed returning passed data if it is holding the revision number encoded', async function () {
173+
it('Initialize call should succeed returning empty bytes even when sending non-empty data as input', async function () {
202174
expect(
203175
await profileFollowModule
204176
.connect(lensHub.address)
205-
.callStatic.initializeFollowModule(FIRST_PROFILE_ID, DEFAULT_INIT_DATA)
206-
).to.eq(DEFAULT_INIT_DATA);
177+
.callStatic.initializeFollowModule(FIRST_PROFILE_ID, abiCoder.encode(['uint256'], [0]))
178+
).to.eq(EMPTY_BYTES);
207179
});
208180

209181
it('Profile creation using profile follow module should succeed and emit expected event', async function () {
@@ -212,7 +184,7 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
212184
handle: MOCK_PROFILE_HANDLE,
213185
imageURI: MOCK_PROFILE_URI,
214186
followModule: profileFollowModule.address,
215-
followModuleInitData: DEFAULT_INIT_DATA,
187+
followModuleInitData: EMPTY_BYTES,
216188
followNFTURI: MOCK_FOLLOW_NFT_URI,
217189
});
218190

@@ -227,7 +199,7 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
227199
MOCK_PROFILE_HANDLE,
228200
MOCK_PROFILE_URI,
229201
profileFollowModule.address,
230-
DEFAULT_INIT_DATA,
202+
EMPTY_BYTES,
231203
MOCK_FOLLOW_NFT_URI,
232204
await getTimestamp(),
233205
]);
@@ -248,7 +220,7 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
248220
const tx = lensHub.setFollowModule(
249221
FIRST_PROFILE_ID,
250222
profileFollowModule.address,
251-
DEFAULT_INIT_DATA
223+
EMPTY_BYTES
252224
);
253225

254226
const receipt = await waitForTx(tx);
@@ -257,7 +229,7 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
257229
matchEvent(receipt, 'FollowModuleSet', [
258230
FIRST_PROFILE_ID,
259231
profileFollowModule.address,
260-
DEFAULT_INIT_DATA,
232+
EMPTY_BYTES,
261233
await getTimestamp(),
262234
]);
263235
});
@@ -271,13 +243,13 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
271243
handle: MOCK_PROFILE_HANDLE,
272244
imageURI: MOCK_PROFILE_URI,
273245
followModule: profileFollowModule.address,
274-
followModuleInitData: DEFAULT_INIT_DATA,
246+
followModuleInitData: EMPTY_BYTES,
275247
followNFTURI: MOCK_FOLLOW_NFT_URI,
276248
})
277249
).to.not.be.reverted;
278250
});
279251

280-
it('Follow call should work when follower profile exists, is owned by the follower address and has not already followed the profile in the current revision', async function () {
252+
it('Follow call should work when follower profile exists, is owned by the follower address and has not already followed the profile', async function () {
281253
await expect(
282254
lensHub.createProfile({
283255
to: userTwoAddress,
@@ -288,30 +260,10 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
288260
followNFTURI: MOCK_FOLLOW_NFT_URI,
289261
})
290262
).to.not.be.reverted;
291-
await expect(
292-
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
293-
).to.not.be.reverted;
294-
});
295-
296-
it('Follow call should work after changing current revision when if it was already followed before by same profile in other revision', async function () {
297-
await expect(
298-
lensHub.createProfile({
299-
to: userTwoAddress,
300-
handle: 'usertwo',
301-
imageURI: MOCK_PROFILE_URI,
302-
followModule: ZERO_ADDRESS,
303-
followModuleInitData: [],
304-
followNFTURI: MOCK_FOLLOW_NFT_URI,
305-
})
306-
).to.not.be.reverted;
307-
await expect(
308-
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
309-
).to.not.be.reverted;
310-
311-
const data = abiCoder.encode(['uint256'], [1]);
312-
await expect(
313-
lensHub.setFollowModule(FIRST_PROFILE_ID, profileFollowModule.address, data)
314-
).to.not.be.reverted;
263+
const followerProfileId = FIRST_PROFILE_ID + 1;
264+
expect(
265+
await profileFollowModule.isProfileFollowing(followerProfileId, FIRST_PROFILE_ID)
266+
).to.be.false;
315267
await expect(
316268
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
317269
).to.not.be.reverted;

0 commit comments

Comments
 (0)