From 0c44b58e6beb05db5f5f6759db482dc9a8e0603a Mon Sep 17 00:00:00 2001 From: Peter Michael Date: Wed, 23 Mar 2022 14:40:12 -0400 Subject: [PATCH 1/2] feat: Implemented profile-specific metadata in the periphery data provider. --- contracts/libraries/DataTypes.sol | 15 ++++ contracts/libraries/Errors.sol | 1 + contracts/libraries/Events.sol | 16 +++- contracts/misc/LensPeripheryDataProvider.sol | 87 +++++++++++++++++++- 4 files changed, 114 insertions(+), 5 deletions(-) diff --git a/contracts/libraries/DataTypes.sol b/contracts/libraries/DataTypes.sol index 33dc25d..83e5b37 100644 --- a/contracts/libraries/DataTypes.sol +++ b/contracts/libraries/DataTypes.sol @@ -342,6 +342,21 @@ library DataTypes { EIP712Signature sig; } + /** + * @notice A struct containing the parameters required for the `setProfileMetadataWithSig()` function. + * + * @param user The user which is the message signer. + * @param profileId The profile ID for which to set the metadata. + * @param metadata The metadata string to set for the profile and user. + * @param sig The EIP712Signature struct containing the user's signature. + */ + struct SetProfileMetadataWithSigData { + address user; + uint256 profileId; + string metadata; + EIP712Signature sig; + } + /** * @notice A struct containing the parameters required for the `toggleFollowWithSig()` function. * diff --git a/contracts/libraries/Errors.sol b/contracts/libraries/Errors.sol index ec75ee6..5d376e3 100644 --- a/contracts/libraries/Errors.sol +++ b/contracts/libraries/Errors.sol @@ -20,6 +20,7 @@ library Errors { error ProfileCreatorNotWhitelisted(); error NotProfileOwner(); error NotProfileOwnerOrDispatcher(); + error NotDispatcher(); error PublicationDoesNotExist(); error HandleTaken(); error HandleLengthInvalid(); diff --git a/contracts/libraries/Events.sol b/contracts/libraries/Events.sol index 2312ce2..bf1b53f 100644 --- a/contracts/libraries/Events.sol +++ b/contracts/libraries/Events.sol @@ -476,7 +476,7 @@ library Events { ); /** - * @dev Emitted when the user wants to enable or disable the follow. + * @dev Emitted when the user wants to enable or disable follows in the `LensPeripheryDataProvider`. * * @param owner The profile owner who executed the toggle. * @param profileIds The array of token IDs of the profiles each followNFT is associated with. @@ -490,4 +490,18 @@ library Events { uint256 timestamp ); + /** + * @dev Emitted when the metadata associated with a profile and user is set in the `LensPeripheryDataProvider`. + * + * @param user The user the metadata is set for. + * @param profileId The profile ID the metadata is set for. + * @param metadata The metadata set for the profile and user. + * @param timestamp The current block timestamp. + */ + event ProfileMetadataSet( + address indexed user, + uint256 indexed profileId, + string metadata, + uint256 timestamp + ); } diff --git a/contracts/misc/LensPeripheryDataProvider.sol b/contracts/misc/LensPeripheryDataProvider.sol index e637ce7..7117590 100644 --- a/contracts/misc/LensPeripheryDataProvider.sol +++ b/contracts/misc/LensPeripheryDataProvider.sol @@ -9,12 +9,12 @@ import {Events} from '../libraries/Events.sol'; import {Errors} from '../libraries/Errors.sol'; /** - * @notice This is a peripheral contract that allows for users to emit an event demonstrating whether or not - * they explicitly want a follow to be shown. + * @notice This is a peripheral contract that acts as a source of truth for profile metadata and allows + * for users to emit an event demonstrating whether or not they explicitly want a follow to be shown. * * @dev This is useful because it allows clients to filter out follow NFTs that were transferred to * a recipient by another user (i.e. Not a mint) and not register them as "following" unless - * the recipient explicitly toggles the follow here. + * the recipient explicitly toggles the follow here. */ contract LensPeripheryDataProvider { string public constant NAME = 'LensPeripheryDataProvider'; @@ -27,15 +27,73 @@ contract LensPeripheryDataProvider { keccak256( 'ToggleFollowWithSig(uint256[] profileIds,bool[] enables,uint256 nonce,uint256 deadline)' ); + bytes32 internal constant SET_PROFILE_METADATA_WITH_SIG_TYPEHASH = + keccak256( + 'SetProfileMetadataWithSig(uint256 profileId,string metadata,uint256 nonce,uint256 deadline)' + ); - ILensHub immutable HUB; + ILensHub public immutable HUB; mapping(address => uint256) public sigNonces; + mapping(address => mapping(uint256 => string)) internal _metadataByProfileByOwner; + constructor(ILensHub hub) { HUB = hub; } + /** + * @notice Sets profile metadata for a profile owner as a dispatcher. + * + * @param profileId The profile ID to set the metadata for. + * @param metadata The metadata string to set for the profile and owner. + */ + function dispatcherSetProfileMetadata(uint256 profileId, string calldata metadata) external { + address owner = IERC721Time(address(HUB)).ownerOf(profileId); + if (msg.sender == HUB.getDispatcher(profileId)) { + _setProfileMetadata(owner, profileId, metadata); + } else { + revert Errors.NotDispatcher(); + } + } + + /** + * @notice Sets the profile metadata for a given profile when owned by the message sender. + * + * @param profileId The profile ID to set the metadata for. + * @param metadata The metadata string to set for the profile and message sender. + */ + function setProfileMetadata(uint256 profileId, string calldata metadata) external { + _setProfileMetadata(msg.sender, profileId, metadata); + } + + /** + * @notice Sets the profile metadata for a given profile and user via signature with the specified parameters. + * + * @param vars A SetProfileMetadataWithSigData struct containingthe regular parameters as well as the user address + * and an EIP712Signature struct. + */ + function setProfileMetadataWithSig(DataTypes.SetProfileMetadataWithSigData calldata vars) + external + { + _validateRecoveredAddress( + _calculateDigest( + keccak256( + abi.encode( + SET_PROFILE_METADATA_WITH_SIG_TYPEHASH, + vars.profileId, + keccak256(bytes(vars.metadata)), + sigNonces[vars.user]++, + vars.sig.deadline + ) + ) + ), + vars.user, + vars.sig + ); + _setProfileMetadata(vars.user, vars.profileId, vars.metadata); + } + /** * @notice Toggle Follows on the given profiles, emiting toggle event for each FollowNFT. * @@ -73,6 +131,27 @@ contract LensPeripheryDataProvider { _toggleFollow(vars.follower, vars.profileIds, vars.enables); } + function getProfileMetadata(uint256 profileId) external view returns (string memory) { + address owner = IERC721Time(address(HUB)).ownerOf(profileId); + return _metadataByProfileByOwner[owner][profileId]; + } + + function getProfileMetadataByOwner(address owner, uint256 profileId) + external + view + returns (string memory) + { + return _metadataByProfileByOwner[owner][profileId]; + } + + function _setProfileMetadata( + address owner, + uint256 profileId, + string calldata metadata + ) internal { + _metadataByProfileByOwner[owner][profileId] = metadata; + } + function _toggleFollow( address follower, uint256[] calldata profileIds, From 1bb385603c7d105208c8114612411b7f43dadb89 Mon Sep 17 00:00:00 2001 From: Peter Michael Date: Fri, 1 Apr 2022 18:41:07 -0400 Subject: [PATCH 2/2] feat: Updated LensPeriphery function names to be more explicit, added tests and moved all LensPeriphery tests to the misc test file. --- contracts/misc/LensPeriphery.sol | 46 +- test/__setup.spec.ts | 5 +- test/helpers/errors.ts | 1 + test/helpers/utils.ts | 38 ++ test/hub/interactions/toggle-follow.spec.ts | 311 ------------ test/other/misc.spec.ts | 526 +++++++++++++++++++- 6 files changed, 594 insertions(+), 333 deletions(-) delete mode 100644 test/hub/interactions/toggle-follow.spec.ts diff --git a/contracts/misc/LensPeriphery.sol b/contracts/misc/LensPeriphery.sol index 9a20477..d190766 100644 --- a/contracts/misc/LensPeriphery.sol +++ b/contracts/misc/LensPeriphery.sol @@ -29,7 +29,7 @@ contract LensPeriphery { ); bytes32 internal constant SET_PROFILE_METADATA_WITH_SIG_TYPEHASH = keccak256( - 'SetProfileMetadataWithSig(uint256 profileId,string metadata,uint256 nonce,uint256 deadline)' + 'SetProfileMetadataURIWithSig(uint256 profileId,string metadata,uint256 nonce,uint256 deadline)' ); ILensHub public immutable HUB; @@ -48,13 +48,10 @@ contract LensPeriphery { * @param profileId The profile ID to set the metadata for. * @param metadata The metadata string to set for the profile and owner. */ - function dispatcherSetProfileMetadata(uint256 profileId, string calldata metadata) external { + function dispatcherSetProfileMetadataURI(uint256 profileId, string calldata metadata) external { address owner = IERC721Time(address(HUB)).ownerOf(profileId); - if (msg.sender == HUB.getDispatcher(profileId)) { - _setProfileMetadata(owner, profileId, metadata); - } else { - revert Errors.NotDispatcher(); - } + if (msg.sender != HUB.getDispatcher(profileId)) revert Errors.NotDispatcher(); + _setProfileMetadataURI(owner, profileId, metadata); } /** @@ -63,8 +60,8 @@ contract LensPeriphery { * @param profileId The profile ID to set the metadata for. * @param metadata The metadata string to set for the profile and message sender. */ - function setProfileMetadata(uint256 profileId, string calldata metadata) external { - _setProfileMetadata(msg.sender, profileId, metadata); + function setProfileMetadataURI(uint256 profileId, string calldata metadata) external { + _setProfileMetadataURI(msg.sender, profileId, metadata); } /** @@ -73,7 +70,7 @@ contract LensPeriphery { * @param vars A SetProfileMetadataWithSigData struct containingthe regular parameters as well as the user address * and an EIP712Signature struct. */ - function setProfileMetadataWithSig(DataTypes.SetProfileMetadataWithSigData calldata vars) + function setProfileMetadataURIWithSig(DataTypes.SetProfileMetadataWithSigData calldata vars) external { _validateRecoveredAddress( @@ -91,7 +88,7 @@ contract LensPeriphery { vars.user, vars.sig ); - _setProfileMetadata(vars.user, vars.profileId, vars.metadata); + _setProfileMetadataURI(vars.user, vars.profileId, vars.metadata); } /** @@ -131,25 +128,40 @@ contract LensPeriphery { _toggleFollow(vars.follower, vars.profileIds, vars.enables); } - function getProfileMetadata(uint256 profileId) external view returns (string memory) { + /** + * @notice Returns the metadata URI of a profile for its current owner. + * + * @param profileId The profile ID to query the metadata URI for. + * + * @return string The metadata associated with that profile ID and the profile's current owner. + */ + function getProfileMetadataURI(uint256 profileId) external view returns (string memory) { address owner = IERC721Time(address(HUB)).ownerOf(profileId); return _metadataByProfileByOwner[owner][profileId]; } - function getProfileMetadataByOwner(address owner, uint256 profileId) + /** + * @notice Returns the metadata URI of a profile for a given user. Note that the user does not *need* to own the + * profile in order to have associated metadata. + * + * @param user The user to query the profile metadata URI for. + * @param profileId The profile ID to query the metadata URI for. + */ + function getProfileMetadataURIByOwner(address user, uint256 profileId) external view returns (string memory) { - return _metadataByProfileByOwner[owner][profileId]; + return _metadataByProfileByOwner[user][profileId]; } - function _setProfileMetadata( - address owner, + function _setProfileMetadataURI( + address user, uint256 profileId, string calldata metadata ) internal { - _metadataByProfileByOwner[owner][profileId] = metadata; + _metadataByProfileByOwner[user][profileId] = metadata; + emit Events.ProfileMetadataSet(user, profileId, metadata, block.timestamp); } function _toggleFollow( diff --git a/test/__setup.spec.ts b/test/__setup.spec.ts index d78ad96..d6db465 100644 --- a/test/__setup.spec.ts +++ b/test/__setup.spec.ts @@ -198,9 +198,8 @@ before(async function () { lensHub = LensHub__factory.connect(proxy.address, user); // LensPeriphery - lensPeriphery = await new LensPeriphery__factory(deployer).deploy( - lensHub.address - ); + lensPeriphery = await new LensPeriphery__factory(deployer).deploy(lensHub.address); + lensPeriphery = lensPeriphery.connect(user); // Currency currency = await new Currency__factory(deployer).deploy(); diff --git a/test/helpers/errors.ts b/test/helpers/errors.ts index 117de0f..2544d0a 100644 --- a/test/helpers/errors.ts +++ b/test/helpers/errors.ts @@ -33,6 +33,7 @@ export const ERRORS = { FOLLOW_NOT_APPROVED: 'FollowNotApproved()', ARRAY_MISMATCH: 'ArrayMismatch()', CANNOT_COMMENT_ON_SELF: 'CannotCommentOnSelf', + NOT_DISPATCHER: 'NotDispatcher()', ERC721_NOT_OWN: 'ERC721: transfer of token that is not own', ERC721_TRANSFER_NOT_OWNER_OR_APPROVED: 'ERC721: transfer caller is not owner nor approved', ERC721_QUERY_FOR_NONEXISTENT_TOKEN: 'ERC721: owner query for nonexistent token', diff --git a/test/helpers/utils.ts b/test/helpers/utils.ts index 0d472e9..ad482d9 100644 --- a/test/helpers/utils.ts +++ b/test/helpers/utils.ts @@ -445,6 +445,16 @@ export async function getToggleFollowWithSigParts( return await getSig(msgParams); } +export async function getSetProfileMetadataURIWithSigParts( + profileId: string | number, + metadata: string, + nonce: number, + deadline: string +): Promise<{ v: number; r: string; s: string }> { + const msgParams = buildSetProfileMetadataURIWithSigParams(profileId, metadata, nonce, deadline); + return await getSig(msgParams); +} + export async function getCollectWithSigParts( profileId: BigNumberish, pubId: string, @@ -863,6 +873,34 @@ const buildToggleFollowWithSigParams = ( }, }); +const buildSetProfileMetadataURIWithSigParams = ( + profileId: string | number, + metadata: string, + nonce: number, + deadline: string +) => ({ + types: { + SetProfileMetadataURIWithSig: [ + { name: 'profileId', type: 'uint256' }, + { name: 'metadata', type: 'string' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }, + domain: { + name: LENS_PERIPHERY_NAME, + version: '1', + chainId: getChainId(), + verifyingContract: lensPeriphery.address, + }, + value: { + profileId: profileId, + metadata: metadata, + nonce: nonce, + deadline: deadline, + }, +}); + const buildCollectWithSigParams = ( profileId: BigNumberish, pubId: string, diff --git a/test/hub/interactions/toggle-follow.spec.ts b/test/hub/interactions/toggle-follow.spec.ts deleted file mode 100644 index 355fb07..0000000 --- a/test/hub/interactions/toggle-follow.spec.ts +++ /dev/null @@ -1,311 +0,0 @@ -import '@nomiclabs/hardhat-ethers'; -import { expect } from 'chai'; -import { FollowNFT__factory } from '../../../typechain-types'; -import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants'; -import { ERRORS } from '../../helpers/errors'; -import { - getAbbreviation, - getFollowWithSigParts, - getTimestamp, - getToggleFollowWithSigParts, - matchEvent, - waitForTx, -} from '../../helpers/utils'; -import { - lensHub, - lensPeriphery, - FIRST_PROFILE_ID, - makeSuiteCleanRoom, - MOCK_PROFILE_HANDLE, - testWallet, - user, - userTwo, - userThree, - userTwoAddress, - userThreeAddress, - MOCK_PROFILE_URI, - userAddress, - MOCK_FOLLOW_NFT_URI, - OTHER_MOCK_URI, -} from '../../__setup.spec'; - -makeSuiteCleanRoom('ToggleFollowing', function () { - beforeEach(async function () { - await expect( - lensHub.createProfile({ - to: userAddress, - handle: MOCK_PROFILE_HANDLE, - imageURI: MOCK_PROFILE_URI, - followModule: ZERO_ADDRESS, - followModuleData: [], - followNFTURI: MOCK_FOLLOW_NFT_URI, - }) - ).to.not.be.reverted; - await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted; - await expect(lensHub.connect(userThree).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted; - await expect(lensHub.connect(testWallet).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted; - }); - context('Generic', function () { - context('Negatives', function () { - it('UserTwo should fail to toggle follow with an incorrect profileId', async function () { - await expect( - lensPeriphery.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 () { - await expect( - lensPeriphery.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; - await expect( - lensPeriphery.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 owned by them', async function () { - const followNFTAddress = await lensHub.getFollowNFT(FIRST_PROFILE_ID); - const followNFT = FollowNFT__factory.connect(followNFTAddress, user); - - await expect( - followNFT.connect(userTwo).transferFrom(userTwoAddress, userAddress, 1) - ).to.not.be.reverted; - - await expect( - lensPeriphery.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 event should be emitted', async function () { - const tx = lensPeriphery.connect(userTwo).toggleFollow([FIRST_PROFILE_ID], [true]); - - const receipt = await waitForTx(tx); - - expect(receipt.logs.length).to.eq(1); - matchEvent(receipt, 'FollowsToggled', [ - userTwoAddress, - [FIRST_PROFILE_ID], - [true], - await getTimestamp(), - ]); - }); - - 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 = lensPeriphery - .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, 'FollowsToggled', [ - userTwoAddress, - [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 tx = lensPeriphery.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(), - ]); - }); - }); - }); - - context('Meta-tx', function () { - context('Negatives', function () { - it('TestWallet should fail to toggle follow with sig with signature deadline mismatch', async function () { - const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber(); - - const { v, r, s } = await getToggleFollowWithSigParts( - [FIRST_PROFILE_ID], - [true], - nonce, - '0' - ); - await expect( - lensPeriphery.toggleFollowWithSig({ - follower: testWallet.address, - profileIds: [FIRST_PROFILE_ID], - enables: [true], - sig: { - v, - r, - s, - deadline: MAX_UINT256, - }, - }) - ).to.be.revertedWith(ERRORS.SIGNATURE_INVALID); - }); - - it('TestWallet should fail to toggle follow with sig with invalid deadline', async function () { - const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber(); - - const { v, r, s } = await getToggleFollowWithSigParts( - [FIRST_PROFILE_ID], - [true], - nonce, - '0' - ); - await expect( - lensPeriphery.toggleFollowWithSig({ - follower: testWallet.address, - profileIds: [FIRST_PROFILE_ID], - enables: [true], - sig: { - v, - r, - s, - deadline: '0', - }, - }) - ).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED); - }); - - it('TestWallet should fail to toggle follow with sig with invalid nonce', async function () { - const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber(); - - const { v, r, s } = await getToggleFollowWithSigParts( - [FIRST_PROFILE_ID], - [true], - nonce + 1, - MAX_UINT256 - ); - - await expect( - lensPeriphery.toggleFollowWithSig({ - follower: testWallet.address, - profileIds: [FIRST_PROFILE_ID], - enables: [true], - sig: { - v, - r, - s, - deadline: MAX_UINT256, - }, - }) - ).to.be.revertedWith(ERRORS.SIGNATURE_INVALID); - }); - - it('TestWallet should fail to toggle follow a nonexistent profile with sig', async function () { - const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber(); - const INVALID_PROFILE = FIRST_PROFILE_ID + 1; - const { v, r, s } = await getToggleFollowWithSigParts( - [INVALID_PROFILE], - [true], - nonce, - MAX_UINT256 - ); - await expect( - lensPeriphery.toggleFollowWithSig({ - follower: testWallet.address, - profileIds: [INVALID_PROFILE], - enables: [true], - sig: { - v, - r, - s, - deadline: MAX_UINT256, - }, - }) - ).to.be.revertedWith(ERRORS.FOLLOW_INVALID); - }); - }); - - context('Scenarios', function () { - it('TestWallet should toggle follow profile 1 to true with sig, correct event should be emitted ', async function () { - const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber(); - - const { v, r, s } = await getToggleFollowWithSigParts( - [FIRST_PROFILE_ID], - [true], - nonce, - MAX_UINT256 - ); - - const tx = lensPeriphery.toggleFollowWithSig({ - follower: testWallet.address, - profileIds: [FIRST_PROFILE_ID], - enables: [true], - sig: { - v, - r, - s, - deadline: MAX_UINT256, - }, - }); - - const receipt = await waitForTx(tx); - - expect(receipt.logs.length).to.eq(1); - matchEvent(receipt, 'FollowsToggled', [ - testWallet.address, - [FIRST_PROFILE_ID], - [true], - await getTimestamp(), - ]); - }); - - it('TestWallet should toggle follow profile 1 to false with sig, correct event should be emitted ', async function () { - const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber(); - - const enabled = false; - const { v, r, s } = await getToggleFollowWithSigParts( - [FIRST_PROFILE_ID], - [enabled], - nonce, - MAX_UINT256 - ); - - const tx = lensPeriphery.toggleFollowWithSig({ - follower: testWallet.address, - profileIds: [FIRST_PROFILE_ID], - enables: [enabled], - sig: { - v, - r, - s, - deadline: MAX_UINT256, - }, - }); - - const receipt = await waitForTx(tx); - - expect(receipt.logs.length).to.eq(1); - matchEvent(receipt, 'FollowsToggled', [ - testWallet.address, - [FIRST_PROFILE_ID], - [enabled], - await getTimestamp(), - ]); - }); - }); - }); -}); diff --git a/test/other/misc.spec.ts b/test/other/misc.spec.ts index 0a67dfa..88730ed 100644 --- a/test/other/misc.spec.ts +++ b/test/other/misc.spec.ts @@ -1,13 +1,18 @@ import '@nomiclabs/hardhat-ethers'; import { expect } from 'chai'; import { keccak256, toUtf8Bytes } from 'ethers/lib/utils'; -import { UIDataProvider__factory } from '../../typechain-types'; -import { ZERO_ADDRESS } from '../helpers/constants'; +import { FollowNFT__factory, UIDataProvider__factory } from '../../typechain-types'; +import { MAX_UINT256, ZERO_ADDRESS } from '../helpers/constants'; import { ERRORS } from '../helpers/errors'; import { getDecodedSvgImage, getMetadataFromBase64TokenUri, + getSetProfileMetadataURIWithSigParts, + getTimestamp, + getToggleFollowWithSigParts, loadTestResourceAsUtf8String, + matchEvent, + waitForTx, } from '../helpers/utils'; import { approvalFollowModule, @@ -35,6 +40,9 @@ import { userTwo, userTwoAddress, abiCoder, + userThree, + testWallet, + lensPeriphery, } from '../__setup.spec'; /** @@ -746,4 +754,518 @@ makeSuiteCleanRoom('Misc', function () { expect(pubByHandleStruct.collectNFT).to.eq(ZERO_ADDRESS); }); }); + + context('LensPeriphery', async function () { + context('ToggleFollowing', function () { + beforeEach(async function () { + await expect( + lensHub.createProfile({ + to: userAddress, + handle: MOCK_PROFILE_HANDLE, + imageURI: MOCK_PROFILE_URI, + followModule: ZERO_ADDRESS, + followModuleData: [], + followNFTURI: MOCK_FOLLOW_NFT_URI, + }) + ).to.not.be.reverted; + await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted; + await expect( + lensHub.connect(userThree).follow([FIRST_PROFILE_ID], [[]]) + ).to.not.be.reverted; + await expect( + lensHub.connect(testWallet).follow([FIRST_PROFILE_ID], [[]]) + ).to.not.be.reverted; + }); + + context('Generic', function () { + context('Negatives', function () { + it('UserTwo should fail to toggle follow with an incorrect profileId', async function () { + await expect( + lensPeriphery.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 () { + await expect( + lensPeriphery.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; + await expect( + lensPeriphery.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 owned by them', async function () { + const followNFTAddress = await lensHub.getFollowNFT(FIRST_PROFILE_ID); + const followNFT = FollowNFT__factory.connect(followNFTAddress, user); + + await expect( + followNFT.connect(userTwo).transferFrom(userTwoAddress, userAddress, 1) + ).to.not.be.reverted; + + await expect( + lensPeriphery.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 event should be emitted', async function () { + const tx = lensPeriphery.connect(userTwo).toggleFollow([FIRST_PROFILE_ID], [true]); + + const receipt = await waitForTx(tx); + + expect(receipt.logs.length).to.eq(1); + matchEvent(receipt, 'FollowsToggled', [ + userTwoAddress, + [FIRST_PROFILE_ID], + [true], + await getTimestamp(), + ]); + }); + + 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 = lensPeriphery + .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, 'FollowsToggled', [ + userTwoAddress, + [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 tx = lensPeriphery.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(), + ]); + }); + }); + }); + + context('Meta-tx', function () { + context('Negatives', function () { + it('TestWallet should fail to toggle follow with sig with signature deadline mismatch', async function () { + const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber(); + + const { v, r, s } = await getToggleFollowWithSigParts( + [FIRST_PROFILE_ID], + [true], + nonce, + '0' + ); + await expect( + lensPeriphery.toggleFollowWithSig({ + follower: testWallet.address, + profileIds: [FIRST_PROFILE_ID], + enables: [true], + sig: { + v, + r, + s, + deadline: MAX_UINT256, + }, + }) + ).to.be.revertedWith(ERRORS.SIGNATURE_INVALID); + }); + + it('TestWallet should fail to toggle follow with sig with invalid deadline', async function () { + const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber(); + + const { v, r, s } = await getToggleFollowWithSigParts( + [FIRST_PROFILE_ID], + [true], + nonce, + '0' + ); + await expect( + lensPeriphery.toggleFollowWithSig({ + follower: testWallet.address, + profileIds: [FIRST_PROFILE_ID], + enables: [true], + sig: { + v, + r, + s, + deadline: '0', + }, + }) + ).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED); + }); + + it('TestWallet should fail to toggle follow with sig with invalid nonce', async function () { + const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber(); + + const { v, r, s } = await getToggleFollowWithSigParts( + [FIRST_PROFILE_ID], + [true], + nonce + 1, + MAX_UINT256 + ); + + await expect( + lensPeriphery.toggleFollowWithSig({ + follower: testWallet.address, + profileIds: [FIRST_PROFILE_ID], + enables: [true], + sig: { + v, + r, + s, + deadline: MAX_UINT256, + }, + }) + ).to.be.revertedWith(ERRORS.SIGNATURE_INVALID); + }); + + it('TestWallet should fail to toggle follow a nonexistent profile with sig', async function () { + const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber(); + const INVALID_PROFILE = FIRST_PROFILE_ID + 1; + const { v, r, s } = await getToggleFollowWithSigParts( + [INVALID_PROFILE], + [true], + nonce, + MAX_UINT256 + ); + await expect( + lensPeriphery.toggleFollowWithSig({ + follower: testWallet.address, + profileIds: [INVALID_PROFILE], + enables: [true], + sig: { + v, + r, + s, + deadline: MAX_UINT256, + }, + }) + ).to.be.revertedWith(ERRORS.FOLLOW_INVALID); + }); + }); + + context('Scenarios', function () { + it('TestWallet should toggle follow profile 1 to true with sig, correct event should be emitted ', async function () { + const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber(); + + const { v, r, s } = await getToggleFollowWithSigParts( + [FIRST_PROFILE_ID], + [true], + nonce, + MAX_UINT256 + ); + + const tx = lensPeriphery.toggleFollowWithSig({ + follower: testWallet.address, + profileIds: [FIRST_PROFILE_ID], + enables: [true], + sig: { + v, + r, + s, + deadline: MAX_UINT256, + }, + }); + + const receipt = await waitForTx(tx); + + expect(receipt.logs.length).to.eq(1); + matchEvent(receipt, 'FollowsToggled', [ + testWallet.address, + [FIRST_PROFILE_ID], + [true], + await getTimestamp(), + ]); + }); + + it('TestWallet should toggle follow profile 1 to false with sig, correct event should be emitted ', async function () { + const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber(); + + const enabled = false; + const { v, r, s } = await getToggleFollowWithSigParts( + [FIRST_PROFILE_ID], + [enabled], + nonce, + MAX_UINT256 + ); + + const tx = lensPeriphery.toggleFollowWithSig({ + follower: testWallet.address, + profileIds: [FIRST_PROFILE_ID], + enables: [enabled], + sig: { + v, + r, + s, + deadline: MAX_UINT256, + }, + }); + + const receipt = await waitForTx(tx); + + expect(receipt.logs.length).to.eq(1); + matchEvent(receipt, 'FollowsToggled', [ + testWallet.address, + [FIRST_PROFILE_ID], + [enabled], + await getTimestamp(), + ]); + }); + }); + }); + }); + + context('Profile Metadata URI', function () { + const MOCK_DATA = 'd171c8b1d364bb34553299ab686caa41ac7a2209d4a63e25947764080c4681da'; + + context('Generic', function () { + beforeEach(async function () { + await expect( + lensHub.createProfile({ + to: userAddress, + handle: MOCK_PROFILE_HANDLE, + imageURI: MOCK_PROFILE_URI, + followModule: ZERO_ADDRESS, + followModuleData: [], + followNFTURI: MOCK_FOLLOW_NFT_URI, + }) + ).to.not.be.reverted; + }); + + context('Negatives', function () { + it("User should fail to set profile metadata URI as dispatcher without being the profile's dispatcher", async function () { + await expect( + lensPeriphery.dispatcherSetProfileMetadataURI(FIRST_PROFILE_ID, MOCK_DATA) + ).to.be.revertedWith(ERRORS.NOT_DISPATCHER); + }); + + it('Fetching profile metadata for a profile that does not exist yet should fail', async function () { + await expect( + lensPeriphery.getProfileMetadataURI(FIRST_PROFILE_ID + 1) + ).to.be.revertedWith(ERRORS.ERC721_QUERY_FOR_NONEXISTENT_TOKEN); + }); + }); + + context('Scenarios', function () { + it('User should set profile metadata for a profile that does not exist, fetched data should be accurate and revert without passing the owner', async function () { + const secondProfileId = FIRST_PROFILE_ID + 1; + await expect( + lensPeriphery.setProfileMetadataURI(secondProfileId, MOCK_DATA) + ).to.not.be.reverted; + + expect( + await lensPeriphery.getProfileMetadataURIByOwner(userAddress, secondProfileId) + ).to.eq(MOCK_DATA); + await expect(lensPeriphery.getProfileMetadataURI(secondProfileId)).to.be.revertedWith( + ERRORS.ERC721_QUERY_FOR_NONEXISTENT_TOKEN + ); + }); + + it("User should set user two as dispatcher, user two should set profile metadata URI for user one's profile, fetched data should be accurate", async function () { + await expect( + lensHub.setDispatcher(FIRST_PROFILE_ID, userTwoAddress) + ).to.not.be.reverted; + await expect( + lensPeriphery + .connect(userTwo) + .dispatcherSetProfileMetadataURI(FIRST_PROFILE_ID, MOCK_DATA) + ).to.not.be.reverted; + + expect( + await lensPeriphery.getProfileMetadataURIByOwner(userAddress, FIRST_PROFILE_ID) + ).to.eq(MOCK_DATA); + expect(await lensPeriphery.getProfileMetadataURI(FIRST_PROFILE_ID)).to.eq(MOCK_DATA); + }); + + it('Setting profile metadata should emit the correct event', async function () { + const tx = await waitForTx( + lensPeriphery.setProfileMetadataURI(FIRST_PROFILE_ID, MOCK_DATA) + ); + + matchEvent(tx, 'ProfileMetadataSet', [ + userAddress, + FIRST_PROFILE_ID, + MOCK_DATA, + await getTimestamp(), + ]); + }); + + it('Setting profile metadata via dispatcher should emit the correct event', async function () { + await expect( + lensHub.setDispatcher(FIRST_PROFILE_ID, userTwoAddress) + ).to.not.be.reverted; + + const tx = await waitForTx( + lensPeriphery + .connect(userTwo) + .dispatcherSetProfileMetadataURI(FIRST_PROFILE_ID, MOCK_DATA) + ); + + matchEvent(tx, 'ProfileMetadataSet', [ + userAddress, + FIRST_PROFILE_ID, + MOCK_DATA, + await getTimestamp(), + ]); + }); + }); + }); + + context('Meta-tx', async function () { + beforeEach(async function () { + await expect( + lensHub.connect(testWallet).createProfile({ + to: testWallet.address, + handle: MOCK_PROFILE_HANDLE, + imageURI: MOCK_PROFILE_URI, + followModule: ZERO_ADDRESS, + followModuleData: [], + followNFTURI: MOCK_FOLLOW_NFT_URI, + }) + ).to.not.be.reverted; + }); + + context('Negatives', async function () { + it('TestWallet should fail to set profile metadata URI with sig with signature deadline mismatch', async function () { + const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber(); + + const { v, r, s } = await getSetProfileMetadataURIWithSigParts( + FIRST_PROFILE_ID, + MOCK_DATA, + nonce, + '0' + ); + await expect( + lensPeriphery.setProfileMetadataURIWithSig({ + user: testWallet.address, + profileId: FIRST_PROFILE_ID, + metadata: MOCK_DATA, + sig: { + v, + r, + s, + deadline: MAX_UINT256, + }, + }) + ).to.be.revertedWith(ERRORS.SIGNATURE_INVALID); + }); + + it('TestWallet should fail to set profile metadata URI with sig with invalid deadline', async function () { + const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber(); + + const { v, r, s } = await getSetProfileMetadataURIWithSigParts( + FIRST_PROFILE_ID, + MOCK_DATA, + nonce, + '0' + ); + await expect( + lensPeriphery.setProfileMetadataURIWithSig({ + user: testWallet.address, + profileId: FIRST_PROFILE_ID, + metadata: MOCK_DATA, + sig: { + v, + r, + s, + deadline: '0', + }, + }) + ).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED); + }); + + it('TestWallet should fail to set profile metadata URI with sig with invalid nonce', async function () { + const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber(); + + const { v, r, s } = await getSetProfileMetadataURIWithSigParts( + FIRST_PROFILE_ID, + MOCK_DATA, + nonce + 1, + MAX_UINT256 + ); + await expect( + lensPeriphery.setProfileMetadataURIWithSig({ + user: testWallet.address, + profileId: FIRST_PROFILE_ID, + metadata: MOCK_DATA, + sig: { + v, + r, + s, + deadline: MAX_UINT256, + }, + }) + ).to.be.revertedWith(ERRORS.SIGNATURE_INVALID); + }); + }); + + context('Scenarios', function () { + it('TestWallet should set profile metadata URI with sig, fetched data should be accurate and correct event should be emitted', async function () { + const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber(); + + const { v, r, s } = await getSetProfileMetadataURIWithSigParts( + FIRST_PROFILE_ID, + MOCK_DATA, + nonce, + MAX_UINT256 + ); + const tx = await waitForTx( + lensPeriphery.setProfileMetadataURIWithSig({ + user: testWallet.address, + profileId: FIRST_PROFILE_ID, + metadata: MOCK_DATA, + sig: { + v, + r, + s, + deadline: MAX_UINT256, + }, + }) + ); + + expect(await lensPeriphery.getProfileMetadataURI(FIRST_PROFILE_ID)).to.eq(MOCK_DATA); + expect( + await lensPeriphery.getProfileMetadataURIByOwner(testWallet.address, FIRST_PROFILE_ID) + ).to.eq(MOCK_DATA); + + matchEvent(tx, 'ProfileMetadataSet', [ + testWallet.address, + FIRST_PROFILE_ID, + MOCK_DATA, + await getTimestamp(), + ]); + }); + }); + }); + }); + }); });