diff --git a/contracts/core/LensHub.sol b/contracts/core/LensHub.sol index 99882df..bdafd8a 100644 --- a/contracts/core/LensHub.sol +++ b/contracts/core/LensHub.sol @@ -566,13 +566,11 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat /// @inheritdoc ILensHub function toggleFollow( uint256[] calldata profileIds, - uint256[] calldata followNFTIds, bool[] calldata enables ) external override whenNotPaused { InteractionLogic.toggleFollow( msg.sender, profileIds, - followNFTIds, enables, _profileById, _profileIdByHandleHash @@ -591,7 +589,6 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat abi.encode( TOGGLE_FOLLOW_WITH_SIG_TYPEHASH, keccak256(abi.encodePacked(vars.profileIds)), - keccak256(abi.encodePacked(vars.followNFTIds)), keccak256(abi.encodePacked(vars.enables)), sigNonces[vars.follower]++, vars.sig.deadline @@ -604,7 +601,6 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat InteractionLogic.toggleFollow( vars.follower, vars.profileIds, - vars.followNFTIds, vars.enables, _profileById, _profileIdByHandleHash diff --git a/contracts/interfaces/ILensHub.sol b/contracts/interfaces/ILensHub.sol index 6d7881f..1828c0f 100644 --- a/contracts/interfaces/ILensHub.sol +++ b/contracts/interfaces/ILensHub.sol @@ -247,14 +247,9 @@ interface ILensHub { * NOTE: `profileIds`, `followNFTIds` and `enables` arrays must be of the same length. * * @param profileIds The token ID array of the profiles. - * @param followNFTIds The token ID array of the followNFTs. * @param enables The array of booleans to enable/disable follows. */ - function toggleFollow( - uint256[] calldata profileIds, - uint256[] calldata followNFTIds, - bool[] calldata enables - ) external; + function toggleFollow(uint256[] calldata profileIds, bool[] calldata enables) external; /** * @notice Toggle Follows a given profiles via signature with the specified parameters. diff --git a/contracts/libraries/DataTypes.sol b/contracts/libraries/DataTypes.sol index 74b6cd7..33dc25d 100644 --- a/contracts/libraries/DataTypes.sol +++ b/contracts/libraries/DataTypes.sol @@ -347,14 +347,12 @@ library DataTypes { * * @param follower The follower which is the message signer. * @param profileIds The token ID array of the profiles. - * @param followNFTIds The token ID array of the followNFTs. * @param enables The array of booleans to enable/disable follows. * @param sig The EIP712Signature struct containing the follower's signature. */ struct ToggleFollowWithSigData { address follower; uint256[] profileIds; - uint256[] followNFTIds; bool[] enables; EIP712Signature sig; } diff --git a/contracts/libraries/InteractionLogic.sol b/contracts/libraries/InteractionLogic.sol index be79c5f..c5bbea2 100644 --- a/contracts/libraries/InteractionLogic.sol +++ b/contracts/libraries/InteractionLogic.sol @@ -161,20 +161,17 @@ library InteractionLogic { * * @param follower The address executing the follow. * @param profileIds The token ID array of the profiles. - * @param followNFTIds The token ID array of the followNFTs. * @param enables The array of booleans to enable/disable follows. * @param _profileById A pointer to the storage mapping of profile structs by profile ID. */ function toggleFollow( address follower, uint256[] calldata profileIds, - uint256[] calldata followNFTIds, bool[] calldata enables, mapping(uint256 => DataTypes.ProfileStruct) storage _profileById, mapping(bytes32 => uint256) storage _profileIdByHandleHash ) external { - if (profileIds.length != followNFTIds.length || profileIds.length != enables.length) - revert Errors.ArrayMismatch(); + if (profileIds.length != enables.length) revert Errors.ArrayMismatch(); for (uint256 i = 0; i < profileIds.length; ++i) { address followNFT = _profileById[profileIds[i]].followNFT; if (followNFT == address(0)) revert Errors.FollowInvalid(); @@ -183,8 +180,7 @@ library InteractionLogic { if (_profileIdByHandleHash[keccak256(bytes(handle))] == 0) revert Errors.TokenDoesNotExist(); - if (ERC721Time(followNFT).ownerOf(followNFTIds[i]) != follower) - revert Errors.FollowInvalid(); + if (ERC721Time(followNFT).balanceOf(follower) == 0) revert Errors.FollowInvalid(); } emit Events.FollowsToggled(follower, profileIds, enables, block.timestamp); } diff --git a/test/helpers/utils.ts b/test/helpers/utils.ts index 75b5786..fab8e9c 100644 --- a/test/helpers/utils.ts +++ b/test/helpers/utils.ts @@ -420,14 +420,12 @@ export async function getFollowWithSigParts( export async function getToggleFollowWithSigParts( profileIds: string[] | number[], - followNFTIds: string[] | number[], enables: boolean[], nonce: number, deadline: string ): Promise<{ v: number; r: string; s: string }> { const msgParams = buildToggleFollowWithSigParams( profileIds, - followNFTIds, enables, nonce, deadline @@ -800,7 +798,6 @@ const buildFollowWithSigParams = ( const buildToggleFollowWithSigParams = ( profileIds: string[] | number[], - followNFTIds: string[] | number[], enables: boolean[], nonce: number, deadline: string @@ -808,7 +805,6 @@ const buildToggleFollowWithSigParams = ( types: { ToggleFollowWithSig: [ { name: 'profileIds', type: 'uint256[]' }, - { name: 'followNFTIds', type: 'uint256[]' }, { name: 'enables', type: 'bool[]' }, { name: 'nonce', type: 'uint256' }, { name: 'deadline', type: 'uint256' }, @@ -817,7 +813,6 @@ const buildToggleFollowWithSigParams = ( domain: domain(), value: { profileIds: profileIds, - followNFTIds: followNFTIds, enables: enables, nonce: nonce, deadline: deadline, diff --git a/test/hub/interactions/toggle-follow.spec.ts b/test/hub/interactions/toggle-follow.spec.ts index ad18b58..7841e88 100644 --- a/test/hub/interactions/toggle-follow.spec.ts +++ b/test/hub/interactions/toggle-follow.spec.ts @@ -25,13 +25,14 @@ import { MOCK_PROFILE_URI, userAddress, MOCK_FOLLOW_NFT_URI, + OTHER_MOCK_URI, } from '../../__setup.spec'; -const getTokenId = async (address) => { - const followNFTAddress = await lensHub.getFollowNFT(FIRST_PROFILE_ID); - const followNFT = FollowNFT__factory.connect(followNFTAddress, user); - return await followNFT.tokenOfOwnerByIndex(address, 0); -}; +// const getTokenId = async (address: string) => { +// const followNFTAddress = await lensHub.getFollowNFT(FIRST_PROFILE_ID); +// const followNFT = FollowNFT__factory.connect(followNFTAddress, user); +// return await followNFT.tokenOfOwnerByIndex(address, 0); +// }; makeSuiteCleanRoom('ToggleFollowing', function () { beforeEach(async function () { @@ -52,63 +53,93 @@ makeSuiteCleanRoom('ToggleFollowing', function () { context('Generic', function () { context('Negatives', function () { it('UserTwo should fail to toggle follow with an incorrect profileId', async function () { - const id = await getTokenId(userTwoAddress); await expect( - lensHub.connect(userTwo).toggleFollow([FIRST_PROFILE_ID + 1], [id], [true]) + lensHub.connect(userTwo).toggleFollow([FIRST_PROFILE_ID + 1], [true]) ).to.be.revertedWith(ERRORS.FOLLOW_INVALID); }); it('UserTwo should fail to toggle follow with array mismatch', async function () { - const id = await getTokenId(userTwoAddress); await expect( - lensHub.connect(userTwo).toggleFollow([FIRST_PROFILE_ID, FIRST_PROFILE_ID], [id], []) + lensHub.connect(userTwo).toggleFollow([FIRST_PROFILE_ID, FIRST_PROFILE_ID], []) ).to.be.revertedWith(ERRORS.ARRAY_MISMATCH); }); it('UserTwo should fail to toggle follow from a profile that has been burned', async function () { await expect(lensHub.burn(FIRST_PROFILE_ID)).to.not.be.reverted; - const id = await getTokenId(userTwoAddress); await expect( - lensHub.connect(userTwo).toggleFollow([FIRST_PROFILE_ID], [id], [true]) + lensHub.connect(userTwo).toggleFollow([FIRST_PROFILE_ID], [true]) ).to.be.revertedWith(ERRORS.TOKEN_DOES_NOT_EXIST); }); - it('UserTwo should fail to toggle follow for a followNFT that is not the owner.', async function () { - const id = await getTokenId(userThreeAddress); + it('UserTwo should fail to toggle follow for a followNFT that is not owned by them', async function () { + const followNFTAddress = await lensHub.getFollowNFT(FIRST_PROFILE_ID); + const followNFT = FollowNFT__factory.connect(followNFTAddress, user); + await expect( - lensHub.connect(userTwo).toggleFollow([FIRST_PROFILE_ID], [id], [true]) + followNFT.connect(userTwo).transferFrom(userTwoAddress, userAddress, 1) + ).to.not.be.reverted; + + await expect( + lensHub.connect(userTwo).toggleFollow([FIRST_PROFILE_ID], [true]) ).to.be.revertedWith(ERRORS.FOLLOW_INVALID); }); }); context('Scenarios', function () { - it('UserTwo should toggle follow with true value, correct events should be emitted', async function () { - const id = await getTokenId(userTwoAddress); - - const tx = lensHub.connect(userTwo).toggleFollow([FIRST_PROFILE_ID], [id], [true]); + it('UserTwo should toggle follow with true value, correct event should be emitted', async function () { + const tx = lensHub.connect(userTwo).toggleFollow([FIRST_PROFILE_ID], [true]); const receipt = await waitForTx(tx); expect(receipt.logs.length).to.eq(1); - matchEvent(receipt, 'ToggleFollowNFT', [ - FIRST_PROFILE_ID, + matchEvent(receipt, 'FollowsToggled', [ userTwoAddress, - true, + [FIRST_PROFILE_ID], + [true], await getTimestamp(), ]); }); - it('UserTwo should toggle follow with false value, correct events should be emitted', async function () { - const id = await getTokenId(userTwoAddress); - const tx = lensHub.connect(userTwo).toggleFollow([FIRST_PROFILE_ID], [id], [false]); + it('User should create another profile, userTwo follows, then toggles both, one true, one false, correct event should be emitted', async function () { + await expect( + lensHub.createProfile({ + to: userAddress, + handle: 'otherhandle', + imageURI: OTHER_MOCK_URI, + followModule: ZERO_ADDRESS, + followModuleData: [], + followNFTURI: MOCK_FOLLOW_NFT_URI, + }) + ).to.not.be.reverted; + await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID + 1], [[]])).to.not.be.reverted; + + const tx = lensHub + .connect(userTwo) + .toggleFollow([FIRST_PROFILE_ID, FIRST_PROFILE_ID + 1], [true, false]); const receipt = await waitForTx(tx); expect(receipt.logs.length).to.eq(1); - matchEvent(receipt, 'ToggleFollowNFT', [ - FIRST_PROFILE_ID, + matchEvent(receipt, 'FollowsToggled', [ userTwoAddress, - false, + [FIRST_PROFILE_ID, FIRST_PROFILE_ID + 1], + [true, false], + await getTimestamp(), + ]); + }); + + it('UserTwo should toggle follow with false value, correct event should be emitted', async function () { + // const id = await getTokenId(userTwoAddress); + + const tx = lensHub.connect(userTwo).toggleFollow([FIRST_PROFILE_ID], [false]); + + const receipt = await waitForTx(tx); + + expect(receipt.logs.length).to.eq(1); + matchEvent(receipt, 'FollowsToggled', [ + userTwoAddress, + [FIRST_PROFILE_ID], + [false], await getTimestamp(), ]); }); @@ -120,11 +151,8 @@ makeSuiteCleanRoom('ToggleFollowing', function () { it('TestWallet should fail to toggle follow with sig with signature deadline mismatch', async function () { const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber(); - const id = await getTokenId(testWallet.address); - const { v, r, s } = await getToggleFollowWithSigParts( [FIRST_PROFILE_ID], - [id.toNumber()], [true], nonce, '0' @@ -133,7 +161,6 @@ makeSuiteCleanRoom('ToggleFollowing', function () { lensHub.toggleFollowWithSig({ follower: testWallet.address, profileIds: [FIRST_PROFILE_ID], - followNFTIds: [id], enables: [true], sig: { v, @@ -148,11 +175,8 @@ makeSuiteCleanRoom('ToggleFollowing', function () { it('TestWallet should fail to toggle follow with sig with invalid deadline', async function () { const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber(); - const id = await getTokenId(testWallet.address); - const { v, r, s } = await getToggleFollowWithSigParts( [FIRST_PROFILE_ID], - [id.toNumber()], [true], nonce, '0' @@ -161,7 +185,6 @@ makeSuiteCleanRoom('ToggleFollowing', function () { lensHub.toggleFollowWithSig({ follower: testWallet.address, profileIds: [FIRST_PROFILE_ID], - followNFTIds: [id], enables: [true], sig: { v, @@ -176,11 +199,8 @@ makeSuiteCleanRoom('ToggleFollowing', function () { it('TestWallet should fail to toggle follow with sig with invalid nonce', async function () { const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber(); - const id = await getTokenId(testWallet.address); - const { v, r, s } = await getToggleFollowWithSigParts( [FIRST_PROFILE_ID], - [id.toNumber()], [true], nonce + 1, MAX_UINT256 @@ -190,7 +210,6 @@ makeSuiteCleanRoom('ToggleFollowing', function () { lensHub.toggleFollowWithSig({ follower: testWallet.address, profileIds: [FIRST_PROFILE_ID], - followNFTIds: [id], enables: [true], sig: { v, @@ -204,12 +223,9 @@ makeSuiteCleanRoom('ToggleFollowing', function () { it('TestWallet should fail to toggle follow a nonexistent profile with sig', async function () { const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber(); - - const id = await getTokenId(testWallet.address); const INVALID_PROFILE = FIRST_PROFILE_ID + 1; const { v, r, s } = await getToggleFollowWithSigParts( [INVALID_PROFILE], - [id.toNumber()], [true], nonce, MAX_UINT256 @@ -218,7 +234,6 @@ makeSuiteCleanRoom('ToggleFollowing', function () { lensHub.toggleFollowWithSig({ follower: testWallet.address, profileIds: [INVALID_PROFILE], - followNFTIds: [id.toNumber()], enables: [true], sig: { v, @@ -232,14 +247,11 @@ makeSuiteCleanRoom('ToggleFollowing', function () { }); context('Scenarios', function () { - it('TestWallet should toggle follow profile 1 to true with sig, correct events should be emitted ', async function () { + it('TestWallet should toggle follow profile 1 to true with sig, correct event should be emitted ', async function () { const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber(); - const id = await getTokenId(testWallet.address); - const { v, r, s } = await getToggleFollowWithSigParts( [FIRST_PROFILE_ID], - [id.toNumber()], [true], nonce, MAX_UINT256 @@ -248,7 +260,6 @@ makeSuiteCleanRoom('ToggleFollowing', function () { const tx = lensHub.toggleFollowWithSig({ follower: testWallet.address, profileIds: [FIRST_PROFILE_ID], - followNFTIds: [id.toNumber()], enables: [true], sig: { v, @@ -261,22 +272,20 @@ makeSuiteCleanRoom('ToggleFollowing', function () { const receipt = await waitForTx(tx); expect(receipt.logs.length).to.eq(1); - matchEvent(receipt, 'ToggleFollowNFT', [ - FIRST_PROFILE_ID, + matchEvent(receipt, 'FollowsToggled', [ testWallet.address, - true, + [FIRST_PROFILE_ID], + [true], await getTimestamp(), ]); }); - it('TestWallet should toggle follow profile 1 to false with sig, correct events should be emitted ', async function () { + it('TestWallet should toggle follow profile 1 to false with sig, correct event should be emitted ', async function () { const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber(); - const id = await getTokenId(testWallet.address); const enabled = false; const { v, r, s } = await getToggleFollowWithSigParts( [FIRST_PROFILE_ID], - [id.toNumber()], [enabled], nonce, MAX_UINT256 @@ -285,7 +294,6 @@ makeSuiteCleanRoom('ToggleFollowing', function () { const tx = lensHub.toggleFollowWithSig({ follower: testWallet.address, profileIds: [FIRST_PROFILE_ID], - followNFTIds: [id.toNumber()], enables: [enabled], sig: { v, @@ -298,10 +306,10 @@ makeSuiteCleanRoom('ToggleFollowing', function () { const receipt = await waitForTx(tx); expect(receipt.logs.length).to.eq(1); - matchEvent(receipt, 'ToggleFollowNFT', [ - FIRST_PROFILE_ID, + matchEvent(receipt, 'FollowsToggled', [ testWallet.address, - enabled, + [FIRST_PROFILE_ID], + [enabled], await getTimestamp(), ]); });