Skip to content

Commit

Permalink
Merge pull request #128 from aave/refactor/profile-follow-module
Browse files Browse the repository at this point in the history
  • Loading branch information
Zer0dot authored May 10, 2022
2 parents 3e41544 + 1785b77 commit 18b3f14
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 95 deletions.
29 changes: 13 additions & 16 deletions contracts/core/modules/follow/ProfileFollowModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,40 +12,38 @@ import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
* @title ProfileFollowModule
* @author Lens Protocol
*
* @notice This follow module only allows profiles that are not already following in the current revision to follow.
* @notice A Lens Profile NFT token-gated follow module with single follow per token validation.
*/
contract ProfileFollowModule is FollowValidatorFollowModuleBase {
mapping(uint256 => mapping(uint256 => mapping(uint256 => bool)))
internal _isProfileFollowingByRevisionByProfile;

mapping(uint256 => uint256) internal _revisionByProfile;
/**
* Given two profile IDs tells if the former has already been used to follow the latter.
*/
mapping(uint256 => mapping(uint256 => bool)) public isProfileFollowing;

constructor(address hub) ModuleBase(hub) {}

/**
* @notice This follow module works on custom profile owner approvals.
*
* @param profileId The profile ID of the profile to initialize this module for.
* @param data The arbitrary data parameter, decoded into:
* uint256 revision: The revision number to be used in this module initialization.
* @param data The arbitrary data parameter, which in this particular module initialization will be just ignored.
*
* @return bytes An abi encoded bytes parameter, which is the same as the passed data parameter.
* @return bytes Empty bytes.
*/
function initializeFollowModule(uint256 profileId, bytes calldata data)
external
override
onlyHub
returns (bytes memory)
{
_revisionByProfile[profileId] = abi.decode(data, (uint256));
return data;
return new bytes(0);
}

/**
* @dev Processes a follow by:
* 1. Validating that the follower owns the profile passed through the data param
* 2. Validating that the profile that is being used to execute the follow is not already following
* the given profile in the current revision
* 1. Validating that the follower owns the profile passed through the data param.
* 2. Validating that the profile that is being used to execute the follow was not already used for following the
* given profile.
*/
function processFollow(
address follower,
Expand All @@ -56,11 +54,10 @@ contract ProfileFollowModule is FollowValidatorFollowModuleBase {
if (IERC721(HUB).ownerOf(followerProfileId) != follower) {
revert Errors.NotProfileOwner();
}
uint256 revision = _revisionByProfile[profileId];
if (_isProfileFollowingByRevisionByProfile[profileId][revision][followerProfileId]) {
if (isProfileFollowing[followerProfileId][profileId]) {
revert Errors.FollowInvalid();
} else {
_isProfileFollowingByRevisionByProfile[profileId][revision][followerProfileId] = true;
isProfileFollowing[followerProfileId][profileId] = true;
}
}

Expand Down
110 changes: 31 additions & 79 deletions test/modules/follow/profile-follow-module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ import {
} from '../../__setup.spec';

makeSuiteCleanRoom('Profile Follow Module', function () {
let DEFAULT_INIT_DATA: BytesLike;
let EMPTY_BYTES: BytesLike;
let DEFAULT_FOLLOW_DATA: BytesLike;

before(async function () {
DEFAULT_INIT_DATA = abiCoder.encode(['uint256'], [0]);
EMPTY_BYTES = '0x';
DEFAULT_FOLLOW_DATA = abiCoder.encode(['uint256'], [FIRST_PROFILE_ID + 1]);
await expect(
lensHub.connect(governance).whitelistFollowModule(profileFollowModule.address, true)
Expand All @@ -37,15 +37,9 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
context('Initialization', function () {
it('Initialize call should fail when sender is not the hub', async function () {
await expect(
profileFollowModule.initializeFollowModule(FIRST_PROFILE_ID, DEFAULT_INIT_DATA)
profileFollowModule.initializeFollowModule(FIRST_PROFILE_ID, EMPTY_BYTES)
).to.be.revertedWith(ERRORS.NOT_HUB);
});

it('Initialize call should fail when data is not holding the revision number encoded', async function () {
await expect(
profileFollowModule.connect(lensHub.address).initializeFollowModule(FIRST_PROFILE_ID, [])
).to.be.reverted;
});
});

context('Following', function () {
Expand All @@ -56,7 +50,7 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
handle: MOCK_PROFILE_HANDLE,
imageURI: MOCK_PROFILE_URI,
followModule: profileFollowModule.address,
followModuleInitData: DEFAULT_INIT_DATA,
followModuleInitData: EMPTY_BYTES,
followNFTURI: MOCK_FOLLOW_NFT_URI,
})
).to.not.be.reverted;
Expand Down Expand Up @@ -118,26 +112,7 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
).to.be.revertedWith(ERRORS.NOT_PROFILE_OWNER);
});

it('Follow should fail when the passed follower profile has already followed the profile in the current revision', async function () {
await expect(
lensHub.createProfile({
to: userTwoAddress,
handle: 'usertwo',
imageURI: MOCK_PROFILE_URI,
followModule: ZERO_ADDRESS,
followModuleInitData: [],
followNFTURI: MOCK_FOLLOW_NFT_URI,
})
).to.not.be.reverted;
await expect(
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
).to.not.be.reverted;
await expect(
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
});

it('Follow should fail when switching to an old revision where the passed follower profile has already followed the profile', async function () {
it('Follow should fail when the passed follower profile has already followed the profile', async function () {
await expect(
lensHub.createProfile({
to: userTwoAddress,
Expand All @@ -151,27 +126,16 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
await expect(
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
).to.not.be.reverted;

// Update the revision
const data = abiCoder.encode(['uint256'], [1]);
await expect(
lensHub.setFollowModule(FIRST_PROFILE_ID, profileFollowModule.address, data)
).to.not.be.reverted;
// We check that profile can be followed again but through callStatic to avoid state-changes
await expect(
lensHub.connect(userTwo).callStatic.follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
).to.not.be.reverted;

// Return the revision to the original, follow should be invalid
await expect(
lensHub.setFollowModule(FIRST_PROFILE_ID, profileFollowModule.address, DEFAULT_INIT_DATA)
).to.not.be.reverted;
const followerProfileId = FIRST_PROFILE_ID + 1;
expect(
await profileFollowModule.isProfileFollowing(followerProfileId, FIRST_PROFILE_ID)
).to.be.true;
await expect(
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
});

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 () {
it('Follow should fail when the passed follower profile has already followed the profile even after the profile nft has been transfered', async function () {
await expect(
lensHub.createProfile({
to: userTwoAddress,
Expand All @@ -185,10 +149,18 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
await expect(
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
).to.not.be.reverted;
const followerProfileId = FIRST_PROFILE_ID + 1;
expect(
await profileFollowModule.isProfileFollowing(followerProfileId, FIRST_PROFILE_ID)
).to.be.true;

await expect(
lensHub.transferFrom(userAddress, userThreeAddress, FIRST_PROFILE_ID)
).to.not.be.reverted;
expect(
await profileFollowModule.isProfileFollowing(followerProfileId, FIRST_PROFILE_ID)
).to.be.true;

await expect(
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
Expand All @@ -198,12 +170,12 @@ makeSuiteCleanRoom('Profile Follow Module', function () {

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

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

Expand All @@ -227,7 +199,7 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
MOCK_PROFILE_HANDLE,
MOCK_PROFILE_URI,
profileFollowModule.address,
DEFAULT_INIT_DATA,
EMPTY_BYTES,
MOCK_FOLLOW_NFT_URI,
await getTimestamp(),
]);
Expand All @@ -248,7 +220,7 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
const tx = lensHub.setFollowModule(
FIRST_PROFILE_ID,
profileFollowModule.address,
DEFAULT_INIT_DATA
EMPTY_BYTES
);

const receipt = await waitForTx(tx);
Expand All @@ -257,7 +229,7 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
matchEvent(receipt, 'FollowModuleSet', [
FIRST_PROFILE_ID,
profileFollowModule.address,
DEFAULT_INIT_DATA,
EMPTY_BYTES,
await getTimestamp(),
]);
});
Expand All @@ -271,13 +243,13 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
handle: MOCK_PROFILE_HANDLE,
imageURI: MOCK_PROFILE_URI,
followModule: profileFollowModule.address,
followModuleInitData: DEFAULT_INIT_DATA,
followModuleInitData: EMPTY_BYTES,
followNFTURI: MOCK_FOLLOW_NFT_URI,
})
).to.not.be.reverted;
});

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 () {
it('Follow call should work when follower profile exists, is owned by the follower address and has not already followed the profile', async function () {
await expect(
lensHub.createProfile({
to: userTwoAddress,
Expand All @@ -288,30 +260,10 @@ makeSuiteCleanRoom('Profile Follow Module', function () {
followNFTURI: MOCK_FOLLOW_NFT_URI,
})
).to.not.be.reverted;
await expect(
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
).to.not.be.reverted;
});

it('Follow call should work after changing current revision when if it was already followed before by same profile in other revision', async function () {
await expect(
lensHub.createProfile({
to: userTwoAddress,
handle: 'usertwo',
imageURI: MOCK_PROFILE_URI,
followModule: ZERO_ADDRESS,
followModuleInitData: [],
followNFTURI: MOCK_FOLLOW_NFT_URI,
})
).to.not.be.reverted;
await expect(
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
).to.not.be.reverted;

const data = abiCoder.encode(['uint256'], [1]);
await expect(
lensHub.setFollowModule(FIRST_PROFILE_ID, profileFollowModule.address, data)
).to.not.be.reverted;
const followerProfileId = FIRST_PROFILE_ID + 1;
expect(
await profileFollowModule.isProfileFollowing(followerProfileId, FIRST_PROFILE_ID)
).to.be.false;
await expect(
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [DEFAULT_FOLLOW_DATA])
).to.not.be.reverted;
Expand Down

0 comments on commit 18b3f14

Please sign in to comment.