Merge pull request #24 from aave/feat/profile-token-uri

This commit is contained in:
Zer0dot
2022-03-16 17:40:18 -04:00
committed by GitHub
20 changed files with 620 additions and 314 deletions

View File

@@ -80,25 +80,21 @@ contract FollowNFT is LensNFTBase, IFollowNFT {
address delegatee,
DataTypes.EIP712Signature calldata sig
) external override {
bytes32 digest;
unchecked {
digest = keccak256(
abi.encodePacked(
'\x19\x01',
_calculateDomainSeparator(),
keccak256(
abi.encode(
DELEGATE_BY_SIG_TYPEHASH,
delegator,
delegatee,
sigNonces[delegator]++,
sig.deadline
)
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
DELEGATE_BY_SIG_TYPEHASH,
delegator,
delegatee,
sigNonces[delegator]++,
sig.deadline
)
)
);
}
_validateRecoveredAddress(digest, delegator, sig);
),
delegator,
sig
);
_delegate(delegator, delegatee);
}
@@ -203,7 +199,7 @@ contract FollowNFT is LensNFTBase, IFollowNFT {
uint256 tokenId
) internal override {
address fromDelegatee = _delegates[from];
address toDelegatee = _delegates[to];
address toDelegatee = _delegates[to];
address followModule = ILensHub(HUB).getFollowModule(_profileId);
_moveDelegate(fromDelegatee, toDelegatee, 1);

View File

@@ -5,10 +5,13 @@ pragma solidity 0.8.10;
import {ILensHub} from '../interfaces/ILensHub.sol';
import {Events} from '../libraries/Events.sol';
import {Helpers} from '../libraries/Helpers.sol';
import {Constants} from '../libraries/Constants.sol';
import {DataTypes} from '../libraries/DataTypes.sol';
import {Errors} from '../libraries/Errors.sol';
import {PublishingLogic} from '../libraries/PublishingLogic.sol';
import {ProfileTokenURILogic} from '../libraries/ProfileTokenURILogic.sol';
import {InteractionLogic} from '../libraries/InteractionLogic.sol';
import {IERC721Enumerable} from '@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol';
import {LensNFTBase} from './base/LensNFTBase.sol';
import {LensMultiState} from './base/LensMultiState.sol';
import {LensHubStorage} from './storage/LensHubStorage.sol';
@@ -167,27 +170,21 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat
override
whenNotPaused
{
bytes32 digest;
unchecked {
digest = keccak256(
abi.encodePacked(
'\x19\x01',
_calculateDomainSeparator(),
keccak256(
abi.encode(
SET_DEFAULT_PROFILE_WITH_SIG_TYPEHASH,
vars.wallet,
vars.profileId,
sigNonces[vars.wallet]++,
vars.sig.deadline
)
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_DEFAULT_PROFILE_WITH_SIG_TYPEHASH,
vars.wallet,
vars.profileId,
sigNonces[vars.wallet]++,
vars.sig.deadline
)
)
);
}
_validateRecoveredAddress(digest, vars.wallet, vars.sig);
),
vars.wallet,
vars.sig
);
_setDefaultProfile(vars.wallet, vars.profileId);
}
@@ -214,27 +211,22 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat
whenNotPaused
{
address owner = ownerOf(vars.profileId);
bytes32 digest;
unchecked {
digest = keccak256(
abi.encodePacked(
'\x19\x01',
_calculateDomainSeparator(),
keccak256(
abi.encode(
SET_FOLLOW_MODULE_WITH_SIG_TYPEHASH,
vars.profileId,
vars.followModule,
keccak256(vars.followModuleData),
sigNonces[owner]++,
vars.sig.deadline
)
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_FOLLOW_MODULE_WITH_SIG_TYPEHASH,
vars.profileId,
vars.followModule,
keccak256(vars.followModuleData),
sigNonces[owner]++,
vars.sig.deadline
)
)
);
}
_validateRecoveredAddress(digest, owner, vars.sig);
),
owner,
vars.sig
);
PublishingLogic.setFollowModule(
vars.profileId,
vars.followModule,
@@ -257,26 +249,21 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat
whenNotPaused
{
address owner = ownerOf(vars.profileId);
bytes32 digest;
unchecked {
digest = keccak256(
abi.encodePacked(
'\x19\x01',
_calculateDomainSeparator(),
keccak256(
abi.encode(
SET_DISPATCHER_WITH_SIG_TYPEHASH,
vars.profileId,
vars.dispatcher,
sigNonces[owner]++,
vars.sig.deadline
)
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_DISPATCHER_WITH_SIG_TYPEHASH,
vars.profileId,
vars.dispatcher,
sigNonces[owner]++,
vars.sig.deadline
)
)
);
}
_validateRecoveredAddress(digest, owner, vars.sig);
),
owner,
vars.sig
);
_setDispatcher(vars.profileId, vars.dispatcher);
}
@@ -297,26 +284,21 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat
whenNotPaused
{
address owner = ownerOf(vars.profileId);
bytes32 digest;
unchecked {
digest = keccak256(
abi.encodePacked(
'\x19\x01',
_calculateDomainSeparator(),
keccak256(
abi.encode(
SET_PROFILE_IMAGE_URI_WITH_SIG_TYPEHASH,
vars.profileId,
keccak256(bytes(vars.imageURI)),
sigNonces[owner]++,
vars.sig.deadline
)
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_PROFILE_IMAGE_URI_WITH_SIG_TYPEHASH,
vars.profileId,
keccak256(bytes(vars.imageURI)),
sigNonces[owner]++,
vars.sig.deadline
)
)
);
}
_validateRecoveredAddress(digest, owner, vars.sig);
),
owner,
vars.sig
);
_setProfileImageURI(vars.profileId, vars.imageURI);
}
@@ -337,26 +319,21 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat
whenNotPaused
{
address owner = ownerOf(vars.profileId);
bytes32 digest;
unchecked {
digest = keccak256(
abi.encodePacked(
'\x19\x01',
_calculateDomainSeparator(),
keccak256(
abi.encode(
SET_FOLLOW_NFT_URI_WITH_SIG_TYPEHASH,
vars.profileId,
keccak256(bytes(vars.followNFTURI)),
sigNonces[owner]++,
vars.sig.deadline
)
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
SET_FOLLOW_NFT_URI_WITH_SIG_TYPEHASH,
vars.profileId,
keccak256(bytes(vars.followNFTURI)),
sigNonces[owner]++,
vars.sig.deadline
)
)
);
}
_validateRecoveredAddress(digest, owner, vars.sig);
),
owner,
vars.sig
);
_setFollowNFTURI(vars.profileId, vars.followNFTURI);
}
@@ -380,30 +357,25 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat
whenPublishingEnabled
{
address owner = ownerOf(vars.profileId);
bytes32 digest;
unchecked {
digest = keccak256(
abi.encodePacked(
'\x19\x01',
_calculateDomainSeparator(),
keccak256(
abi.encode(
POST_WITH_SIG_TYPEHASH,
vars.profileId,
keccak256(bytes(vars.contentURI)),
vars.collectModule,
keccak256(vars.collectModuleData),
vars.referenceModule,
keccak256(vars.referenceModuleData),
sigNonces[owner]++,
vars.sig.deadline
)
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
POST_WITH_SIG_TYPEHASH,
vars.profileId,
keccak256(bytes(vars.contentURI)),
vars.collectModule,
keccak256(vars.collectModuleData),
vars.referenceModule,
keccak256(vars.referenceModuleData),
sigNonces[owner]++,
vars.sig.deadline
)
)
);
}
_validateRecoveredAddress(digest, owner, vars.sig);
),
owner,
vars.sig
);
_createPost(
vars.profileId,
vars.contentURI,
@@ -427,32 +399,27 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat
whenPublishingEnabled
{
address owner = ownerOf(vars.profileId);
bytes32 digest;
unchecked {
digest = keccak256(
abi.encodePacked(
'\x19\x01',
_calculateDomainSeparator(),
keccak256(
abi.encode(
COMMENT_WITH_SIG_TYPEHASH,
vars.profileId,
keccak256(bytes(vars.contentURI)),
vars.profileIdPointed,
vars.pubIdPointed,
vars.collectModule,
keccak256(vars.collectModuleData),
vars.referenceModule,
keccak256(vars.referenceModuleData),
sigNonces[owner]++,
vars.sig.deadline
)
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
COMMENT_WITH_SIG_TYPEHASH,
vars.profileId,
keccak256(bytes(vars.contentURI)),
vars.profileIdPointed,
vars.pubIdPointed,
vars.collectModule,
keccak256(vars.collectModuleData),
vars.referenceModule,
keccak256(vars.referenceModuleData),
sigNonces[owner]++,
vars.sig.deadline
)
)
);
}
_validateRecoveredAddress(digest, owner, vars.sig);
),
owner,
vars.sig
);
_createComment(
DataTypes.CommentData(
vars.profileId,
@@ -486,29 +453,24 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat
whenPublishingEnabled
{
address owner = ownerOf(vars.profileId);
bytes32 digest;
unchecked {
digest = keccak256(
abi.encodePacked(
'\x19\x01',
_calculateDomainSeparator(),
keccak256(
abi.encode(
MIRROR_WITH_SIG_TYPEHASH,
vars.profileId,
vars.profileIdPointed,
vars.pubIdPointed,
vars.referenceModule,
keccak256(vars.referenceModuleData),
sigNonces[owner]++,
vars.sig.deadline
)
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
MIRROR_WITH_SIG_TYPEHASH,
vars.profileId,
vars.profileIdPointed,
vars.pubIdPointed,
vars.referenceModule,
keccak256(vars.referenceModuleData),
sigNonces[owner]++,
vars.sig.deadline
)
)
);
}
_validateRecoveredAddress(digest, owner, vars.sig);
),
owner,
vars.sig
);
_createMirror(
vars.profileId,
vars.profileIdPointed,
@@ -576,27 +538,21 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat
for (uint256 i = 0; i < vars.datas.length; ++i) {
dataHashes[i] = keccak256(vars.datas[i]);
}
bytes32 digest;
unchecked {
digest = keccak256(
abi.encodePacked(
'\x19\x01',
_calculateDomainSeparator(),
keccak256(
abi.encode(
FOLLOW_WITH_SIG_TYPEHASH,
keccak256(abi.encodePacked(vars.profileIds)),
keccak256(abi.encodePacked(dataHashes)),
sigNonces[vars.follower]++,
vars.sig.deadline
)
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
FOLLOW_WITH_SIG_TYPEHASH,
keccak256(abi.encodePacked(vars.profileIds)),
keccak256(abi.encodePacked(dataHashes)),
sigNonces[vars.follower]++,
vars.sig.deadline
)
)
);
}
_validateRecoveredAddress(digest, vars.follower, vars.sig);
),
vars.follower,
vars.sig
);
InteractionLogic.follow(
vars.follower,
vars.profileIds,
@@ -630,27 +586,22 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat
override
whenNotPaused
{
bytes32 digest;
unchecked {
digest = keccak256(
abi.encodePacked(
'\x19\x01',
_calculateDomainSeparator(),
keccak256(
abi.encode(
COLLECT_WITH_SIG_TYPEHASH,
vars.profileId,
vars.pubId,
keccak256(vars.data),
sigNonces[vars.collector]++,
vars.sig.deadline
)
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
COLLECT_WITH_SIG_TYPEHASH,
vars.profileId,
vars.pubId,
keccak256(vars.data),
sigNonces[vars.collector]++,
vars.sig.deadline
)
)
);
}
_validateRecoveredAddress(digest, vars.collector, vars.sig);
),
vars.collector,
vars.sig
);
InteractionLogic.collect(
vars.collector,
vars.profileId,
@@ -880,7 +831,15 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat
* @dev Overrides the ERC721 tokenURI function to return the associated URI with a given profile.
*/
function tokenURI(uint256 tokenId) public view override returns (string memory) {
return _profileById[tokenId].imageURI; // temp
address followNFT = _profileById[tokenId].followNFT;
return
ProfileTokenURILogic.getProfileTokenURI(
tokenId,
followNFT == address(0) ? 0 : IERC721Enumerable(followNFT).totalSupply(),
ownerOf(tokenId),
_profileById[tokenId].handle,
_profileById[tokenId].imageURI
);
}
/// ****************************
@@ -961,6 +920,8 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat
}
function _setProfileImageURI(uint256 profileId, string memory imageURI) internal {
if (bytes(imageURI).length > Constants.MAX_PROFILE_IMAGE_URI_LENGTH)
revert Errors.ProfileImageURILengthInvalid();
_profileById[profileId].imageURI = imageURI;
emit Events.ProfileImageURISet(profileId, imageURI, block.timestamp);
}

View File

@@ -55,27 +55,15 @@ abstract contract LensNFTBase is ILensNFTBase, ERC721Enumerable {
) external override {
if (spender == address(0)) revert Errors.ZeroSpender();
address owner = ownerOf(tokenId);
bytes32 digest;
unchecked {
digest = keccak256(
abi.encodePacked(
'\x19\x01',
_calculateDomainSeparator(),
keccak256(
abi.encode(
PERMIT_TYPEHASH,
spender,
tokenId,
sigNonces[owner]++,
sig.deadline
)
)
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(PERMIT_TYPEHASH, spender, tokenId, sigNonces[owner]++, sig.deadline)
)
);
}
_validateRecoveredAddress(digest, owner, sig);
),
owner,
sig
);
_approve(spender, tokenId);
}
@@ -87,28 +75,22 @@ abstract contract LensNFTBase is ILensNFTBase, ERC721Enumerable {
DataTypes.EIP712Signature calldata sig
) external override {
if (operator == address(0)) revert Errors.ZeroSpender();
bytes32 digest;
unchecked {
digest = keccak256(
abi.encodePacked(
'\x19\x01',
_calculateDomainSeparator(),
keccak256(
abi.encode(
PERMIT_FOR_ALL_TYPEHASH,
owner,
operator,
approved,
sigNonces[owner]++,
sig.deadline
)
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(
PERMIT_FOR_ALL_TYPEHASH,
owner,
operator,
approved,
sigNonces[owner]++,
sig.deadline
)
)
);
}
_validateRecoveredAddress(digest, owner, sig);
),
owner,
sig
);
_setOperatorApproval(owner, operator, approved);
}
@@ -131,25 +113,15 @@ abstract contract LensNFTBase is ILensNFTBase, ERC721Enumerable {
{
address owner = ownerOf(tokenId);
bytes32 digest;
unchecked {
digest = keccak256(
abi.encodePacked(
'\x19\x01',
_calculateDomainSeparator(),
keccak256(
abi.encode(
BURN_WITH_SIG_TYPEHASH,
tokenId,
sigNonces[owner]++,
sig.deadline
)
)
_validateRecoveredAddress(
_calculateDigest(
keccak256(
abi.encode(BURN_WITH_SIG_TYPEHASH, tokenId, sigNonces[owner]++, sig.deadline)
)
);
}
_validateRecoveredAddress(digest, owner, sig);
),
owner,
sig
);
_burn(tokenId);
}
@@ -182,4 +154,21 @@ abstract contract LensNFTBase is ILensNFTBase, ERC721Enumerable {
)
);
}
/**
* @dev Calculates EIP712 digest based on the current DOMAIN_SEPARATOR.
*
* @param hashedMessage The message hash from which the digest should be calculated.
*
* @return A 32-byte output representing the EIP712 digest.
*/
function _calculateDigest(bytes32 hashedMessage) internal view returns (bytes32) {
bytes32 digest;
unchecked {
digest = keccak256(
abi.encodePacked('\x19\x01', _calculateDomainSeparator(), hashedMessage)
);
}
return digest;
}
}

View File

@@ -8,4 +8,5 @@ library Constants {
string internal constant COLLECT_NFT_NAME_INFIX = '-Collect-';
string internal constant COLLECT_NFT_SYMBOL_INFIX = '-Cl-';
uint8 internal constant MAX_HANDLE_LENGTH = 31;
uint16 internal constant MAX_PROFILE_IMAGE_URI_LENGTH = 6000;
}

View File

@@ -24,6 +24,7 @@ library Errors {
error HandleTaken();
error HandleLengthInvalid();
error HandleContainsInvalidCharacters();
error ProfileImageURILengthInvalid();
error CallerNotFollowNFT();
error CallerNotCollectNFT();
error BlockNumberInvalid();

File diff suppressed because one or more lines are too long

11
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "AGPL-3.0-only",
"dependencies": {
"@openzeppelin/contracts": "4.4.0"
"@openzeppelin/contracts": "4.5.0"
},
"devDependencies": {
"@nomiclabs/hardhat-ethers": "2.0.2",
@@ -3839,8 +3839,9 @@
}
},
"node_modules/@openzeppelin/contracts": {
"version": "4.4.0",
"license": "MIT"
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.5.0.tgz",
"integrity": "sha512-fdkzKPYMjrRiPK6K4y64e6GzULR7R7RwxSigHS8DDp7aWDeoReqsQI+cxHV1UuhAqX69L1lAaWDxenfP+xiqzA=="
},
"node_modules/@resolver-engine/core": {
"version": "0.3.3",
@@ -26166,7 +26167,9 @@
}
},
"@openzeppelin/contracts": {
"version": "4.4.0"
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.5.0.tgz",
"integrity": "sha512-fdkzKPYMjrRiPK6K4y64e6GzULR7R7RwxSigHS8DDp7aWDeoReqsQI+cxHV1UuhAqX69L1lAaWDxenfP+xiqzA=="
},
"@resolver-engine/core": {
"version": "0.3.3",

View File

@@ -49,7 +49,6 @@
"eslint": "8.3.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-prettier": "4.0.0",
"prettier-plugin-solidity": "1.0.0-beta.19",
"ethereum-waffle": "3.4.0",
"ethers": "5.5.1",
"hardhat": "2.7.0",
@@ -59,6 +58,7 @@
"hardhat-spdx-license-identifier": "2.0.3",
"husky": "7.0.4",
"prettier": "2.5.0",
"prettier-plugin-solidity": "1.0.0-beta.19",
"solidity-coverage": "0.7.17",
"ts-generator": "0.1.1",
"ts-node": "10.4.0",
@@ -71,7 +71,7 @@
}
},
"dependencies": {
"@openzeppelin/contracts": "4.4.0"
"@openzeppelin/contracts": "4.5.0"
},
"author": "Lens",
"contributors": [

View File

@@ -20,6 +20,7 @@ import {
RevertCollectModule__factory,
TimedFeeCollectModule__factory,
TransparentUpgradeableProxy__factory,
ProfileTokenURILogic__factory,
} from '../typechain-types';
import { deployWithVerify, waitForTx } from './helpers/utils';
@@ -79,9 +80,16 @@ task('full-deploy-verify', 'deploys the entire Lens Protocol with explorer verif
[],
'contracts/libraries/InteractionLogic.sol:InteractionLogic'
);
const profileTokenURILogic = await deployWithVerify(
new ProfileTokenURILogic__factory(deployer).deploy({ nonce: deployerNonce++ }),
[],
'contracts/libraries/ProfileTokenURILogic.sol:ProfileTokenURILogic'
);
const hubLibs = {
'contracts/libraries/PublishingLogic.sol:PublishingLogic': publishingLogic.address,
'contracts/libraries/InteractionLogic.sol:InteractionLogic': interactionLogic.address,
'contracts/libraries/ProfileTokenURILogic.sol:ProfileTokenURILogic':
profileTokenURILogic.address,
};
// Here, we pre-compute the nonces and addresses used to deploy the contracts.
@@ -279,6 +287,7 @@ task('full-deploy-verify', 'deploys the entire Lens Protocol with explorer verif
'lensHub impl:': lensHubImpl.address,
'publishing logic lib': publishingLogic.address,
'interaction logic lib': interactionLogic.address,
'profile token URI logic lib': profileTokenURILogic.address,
'follow NFT impl': followNFTImplAddress,
'collect NFT impl': collectNFTImplAddress,
'module globals': moduleGlobals.address,

View File

@@ -20,6 +20,7 @@ import {
RevertCollectModule__factory,
TimedFeeCollectModule__factory,
TransparentUpgradeableProxy__factory,
ProfileTokenURILogic__factory,
} from '../typechain-types';
import { deployContract, waitForTx } from './helpers/utils';
@@ -57,9 +58,14 @@ task('full-deploy', 'deploys the entire Lens Protocol').setAction(async ({}, hre
const interactionLogic = await deployContract(
new InteractionLogic__factory(deployer).deploy({ nonce: deployerNonce++ })
);
const profileTokenURILogic = await deployContract(
new ProfileTokenURILogic__factory(deployer).deploy({ nonce: deployerNonce++ })
);
const hubLibs = {
'contracts/libraries/PublishingLogic.sol:PublishingLogic': publishingLogic.address,
'contracts/libraries/InteractionLogic.sol:InteractionLogic': interactionLogic.address,
'contracts/libraries/ProfileTokenURILogic.sol:ProfileTokenURILogic':
profileTokenURILogic.address,
};
// Here, we pre-compute the nonces and addresses used to deploy the contracts.
@@ -72,7 +78,8 @@ task('full-deploy', 'deploys the entire Lens Protocol').setAction(async ({}, hre
'0x' + keccak256(RLP.encode([deployer.address, followNFTNonce])).substr(26);
const collectNFTImplAddress =
'0x' + keccak256(RLP.encode([deployer.address, collectNFTNonce])).substr(26);
const hubProxyAddress = '0x' + keccak256(RLP.encode([deployer.address, hubProxyNonce])).substr(26);
const hubProxyAddress =
'0x' + keccak256(RLP.encode([deployer.address, hubProxyNonce])).substr(26);
// Next, we deploy first the hub implementation, then the followNFT implementation, the collectNFT, and finally the
// hub proxy with initialization.
@@ -187,7 +194,9 @@ task('full-deploy', 'deploys the entire Lens Protocol').setAction(async ({}, hre
})
);
await waitForTx(
lensHub.whitelistCollectModule(timedFeeCollectModule.address, true, { nonce: governanceNonce++ })
lensHub.whitelistCollectModule(timedFeeCollectModule.address, true, {
nonce: governanceNonce++,
})
);
await waitForTx(
lensHub.whitelistCollectModule(limitedTimedFeeCollectModule.address, true, {

View File

@@ -37,6 +37,7 @@ import {
MockReferenceModule__factory,
ModuleGlobals,
ModuleGlobals__factory,
ProfileTokenURILogic__factory,
PublishingLogic__factory,
RevertCollectModule,
RevertCollectModule__factory,
@@ -59,18 +60,17 @@ export const CURRENCY_MINT_AMOUNT = parseEther('100');
export const BPS_MAX = 10000;
export const TREASURY_FEE_BPS = 50;
export const REFERRAL_FEE_BPS = 250;
export const MAX_PROFILE_IMAGE_URI_LENGTH = 6000;
export const LENS_HUB_NFT_NAME = 'Lens Profiles';
export const LENS_HUB_NFT_SYMBOL = 'LENS';
export const MOCK_PROFILE_HANDLE = 'plant1ghost.eth';
export const FIRST_PROFILE_ID = 1;
export const MOCK_URI =
'https://ipfs.fleek.co/ipfs/plantghostplantghostplantghostplantghostplantghostplantghos';
export const OTHER_MOCK_URI =
'https://ipfs.fleek.co/ipfs/ghostplantghostplantghostplantghostplantghostplantghostplan';
export const MOCK_URI = 'https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR';
export const OTHER_MOCK_URI = 'https://ipfs.io/ipfs/QmSfyMcnh1wnJHrAWCBjZHapTS859oNSsuDFiAPPdAHgHP';
export const MOCK_PROFILE_URI =
'https://ipfs.fleek.co/ipfs/runningoutofthingstowriterunningoutofthingstowriterunningou';
'https://ipfs.io/ipfs/Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu';
export const MOCK_FOLLOW_NFT_URI =
'https://ipfs.fleek.co/ipfs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
'https://ipfs.fleek.co/ipfs/ghostplantghostplantghostplantghostplantghostplantghostplan';
export let accounts: Signer[];
export let deployer: Signer;
@@ -149,9 +149,12 @@ before(async function () {
);
const publishingLogic = await new PublishingLogic__factory(deployer).deploy();
const interactionLogic = await new InteractionLogic__factory(deployer).deploy();
const profileTokenURILogic = await new ProfileTokenURILogic__factory(deployer).deploy();
hubLibs = {
'contracts/libraries/PublishingLogic.sol:PublishingLogic': publishingLogic.address,
'contracts/libraries/InteractionLogic.sol:InteractionLogic': interactionLogic.address,
'contracts/libraries/ProfileTokenURILogic.sol:ProfileTokenURILogic':
profileTokenURILogic.address,
};
// Here, we pre-compute the nonces and addresses used to deploy the contracts.

View File

@@ -18,6 +18,7 @@ export const ERRORS = {
PUBLICATION_DOES_NOT_EXIST: 'PublicationDoesNotExist()',
PROFILE_HANDLE_TAKEN: 'HandleTaken()',
INVALID_HANDLE_LENGTH: 'HandleLengthInvalid()',
INVALID_IMAGE_URI_LENGTH: 'ProfileImageURILengthInvalid()',
HANDLE_CONTAINS_INVALID_CHARACTERS: 'HandleContainsInvalidCharacters()',
NOT_FOLLOW_NFT: 'CallerNotFollowNFT()',
NOT_COLLECT_NFT: 'CallerNotCollectNFT()',
@@ -35,6 +36,7 @@ export const ERRORS = {
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',
ERC20_TRANSFER_EXCEEDS_ALLOWANCE: 'ERC20: transfer amount exceeds allowance',
ERC20_INSUFFICIENT_ALLOWANCE: 'ERC20: insufficient allowance',
NO_SELECTOR:
"Transaction reverted: function selector was not recognized and there's no fallback function",
PAUSED: 'Paused()',

View File

@@ -1,12 +1,12 @@
import { TransactionReceipt, TransactionResponse } from '@ethersproject/providers';
import '@nomiclabs/hardhat-ethers';
import { expect } from 'chai';
import { BigNumber, BigNumberish, Bytes, Contract, logger, utils } from 'ethers';
import { hexlify, keccak256, RLP, toUtf8Bytes } from 'ethers/lib/utils';
import hre from 'hardhat';
import { LensHub__factory } from '../../typechain-types';
import { BigNumberish, Bytes, logger, utils, BigNumber, Contract } from 'ethers';
import { eventsLib, helper, lensHub, LENS_HUB_NFT_NAME, testWallet } from '../__setup.spec';
import { expect } from 'chai';
import { HARDHAT_CHAINID, MAX_UINT256 } from './constants';
import { hexlify, keccak256, RLP, toUtf8Bytes } from 'ethers/lib/utils';
import { LensHub__factory } from '../../typechain-types';
import { TransactionReceipt, TransactionResponse } from '@ethersproject/providers';
import hre, { ethers } from 'hardhat';
export enum ProtocolState {
Unpaused,
@@ -429,6 +429,18 @@ export async function getCollectWithSigParts(
return await getSig(msgParams);
}
export async function getJsonMetadataFromBase64TokenUri(tokenUri: string) {
const splittedTokenUri = tokenUri.split('data:application/json;base64,');
if (splittedTokenUri.length != 2) {
logger.throwError('Wrong or unrecognized token URI format');
} else {
const jsonMetadataBase64String = splittedTokenUri[1];
const jsonMetadataBytes = ethers.utils.base64.decode(jsonMetadataBase64String);
const jsonMetadataString = ethers.utils.toUtf8String(jsonMetadataBytes);
return JSON.parse(jsonMetadataString);
}
}
// Modified from AaveTokenV2 repo
const buildPermitParams = (
nft: string,

View File

@@ -1,10 +1,12 @@
import '@nomiclabs/hardhat-ethers';
import { expect } from 'chai';
import { keccak256, toUtf8Bytes } from 'ethers/lib/utils';
import { FollowNFT__factory } from '../../../typechain-types';
import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
import { ERRORS } from '../../helpers/errors';
import {
cancelWithPermitForAll,
getJsonMetadataFromBase64TokenUri,
getSetFollowNFTURIWithSigParts,
getSetProfileImageURIWithSigParts,
} from '../../helpers/utils';
@@ -12,6 +14,7 @@ import {
FIRST_PROFILE_ID,
lensHub,
makeSuiteCleanRoom,
MAX_PROFILE_IMAGE_URI_LENGTH,
MOCK_FOLLOW_NFT_URI,
MOCK_PROFILE_HANDLE,
MOCK_PROFILE_URI,
@@ -46,6 +49,14 @@ makeSuiteCleanRoom('Profile URI Functionality', function () {
).to.be.revertedWith(ERRORS.NOT_PROFILE_OWNER_OR_DISPATCHER);
});
it('UserTwo should fail to set the profile URI on profile owned by user 1', async function () {
const profileURITooLong = MOCK_URI.repeat(500);
expect(profileURITooLong.length).to.be.greaterThan(MAX_PROFILE_IMAGE_URI_LENGTH);
await expect(
lensHub.setProfileImageURI(FIRST_PROFILE_ID, profileURITooLong)
).to.be.revertedWith(ERRORS.INVALID_IMAGE_URI_LENGTH);
});
it('UserTwo should fail to change the follow NFT URI for profile one', async function () {
await expect(
lensHub.connect(userTwo).setFollowNFTURI(FIRST_PROFILE_ID, OTHER_MOCK_URI)
@@ -54,9 +65,134 @@ makeSuiteCleanRoom('Profile URI Functionality', function () {
});
context('Scenarios', function () {
it('User should set the profile URI', async function () {
it('User should have a custom picture tokenURI after setting the profile imageURI', async function () {
await expect(lensHub.setProfileImageURI(FIRST_PROFILE_ID, MOCK_URI)).to.not.be.reverted;
expect(await lensHub.tokenURI(FIRST_PROFILE_ID)).to.eq(MOCK_URI);
const tokenURI = await lensHub.tokenURI(FIRST_PROFILE_ID);
const jsonMetadata = await getJsonMetadataFromBase64TokenUri(tokenURI);
expect(jsonMetadata.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(jsonMetadata.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
const expectedAttributes = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '0' },
{ trait_type: 'owner', value: userAddress.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(jsonMetadata.attributes).to.eql(expectedAttributes);
expect(keccak256(toUtf8Bytes(tokenURI))).to.eq(
'0xe1731460db6ce5fa9f3a9f2bd778a8af49e623dceb531df6b1a5c162b7c2d79a'
);
});
it('Default image should be used when no imageURI set', async function () {
await expect(lensHub.setProfileImageURI(FIRST_PROFILE_ID, '')).to.not.be.reverted;
const tokenURI = await lensHub.tokenURI(FIRST_PROFILE_ID);
const jsonMetadata = await getJsonMetadataFromBase64TokenUri(tokenURI);
expect(jsonMetadata.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(jsonMetadata.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
const expectedAttributes = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '0' },
{ trait_type: 'owner', value: userAddress.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(jsonMetadata.attributes).to.eql(expectedAttributes);
expect(keccak256(toUtf8Bytes(tokenURI))).to.eq(
'0xa21f2a3aa9300a248d3a7acd3f4ff309291653121df87ffe3be545fa1dbd65e5'
);
});
it('Default image should be used when imageURI contains double-quotes', async function () {
const imageURI =
'https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrztRLMiMPL8wBuTGsMnR" <rect x="10" y="10" fill="red';
await expect(lensHub.setProfileImageURI(FIRST_PROFILE_ID, imageURI)).to.not.be.reverted;
const tokenURI = await lensHub.tokenURI(FIRST_PROFILE_ID);
const jsonMetadata = await getJsonMetadataFromBase64TokenUri(tokenURI);
expect(jsonMetadata.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(jsonMetadata.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
const expectedAttributes = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '0' },
{ trait_type: 'owner', value: userAddress.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(jsonMetadata.attributes).to.eql(expectedAttributes);
expect(keccak256(toUtf8Bytes(tokenURI))).to.eq(
'0xa21f2a3aa9300a248d3a7acd3f4ff309291653121df87ffe3be545fa1dbd65e5'
);
});
it('Should return the correct tokenURI after transfer', async function () {
const tokenURIBeforeTransfer = await lensHub.tokenURI(FIRST_PROFILE_ID);
const jsonMetadataBeforeTransfer = await getJsonMetadataFromBase64TokenUri(
tokenURIBeforeTransfer
);
expect(jsonMetadataBeforeTransfer.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(jsonMetadataBeforeTransfer.description).to.eq(
`@${MOCK_PROFILE_HANDLE} - Lens profile`
);
const expectedAttributesBeforeTransfer = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '0' },
{ trait_type: 'owner', value: userAddress.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(jsonMetadataBeforeTransfer.attributes).to.eql(expectedAttributesBeforeTransfer);
await expect(
lensHub.transferFrom(userAddress, userTwoAddress, FIRST_PROFILE_ID)
).to.not.be.reverted;
const tokenURIAfterTransfer = await lensHub.tokenURI(FIRST_PROFILE_ID);
const jsonMetadataAfterTransfer = await getJsonMetadataFromBase64TokenUri(
tokenURIAfterTransfer
);
expect(jsonMetadataAfterTransfer.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(jsonMetadataAfterTransfer.description).to.eq(
`@${MOCK_PROFILE_HANDLE} - Lens profile`
);
const expectedAttributesAfterTransfer = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '0' },
{ trait_type: 'owner', value: userTwoAddress.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(jsonMetadataAfterTransfer.attributes).to.eql(expectedAttributesAfterTransfer);
});
it('Should return the correct tokenURI after a follow', async function () {
const tokenURIBeforeTransfer = await lensHub.tokenURI(FIRST_PROFILE_ID);
const jsonMetadataBeforeTransfer = await getJsonMetadataFromBase64TokenUri(
tokenURIBeforeTransfer
);
expect(jsonMetadataBeforeTransfer.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(jsonMetadataBeforeTransfer.description).to.eq(
`@${MOCK_PROFILE_HANDLE} - Lens profile`
);
const expectedAttributesBeforeTransfer = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '0' },
{ trait_type: 'owner', value: userAddress.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(jsonMetadataBeforeTransfer.attributes).to.eql(expectedAttributesBeforeTransfer);
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
const tokenURIAfterTransfer = await lensHub.tokenURI(FIRST_PROFILE_ID);
const jsonMetadataAfterTransfer = await getJsonMetadataFromBase64TokenUri(
tokenURIAfterTransfer
);
expect(jsonMetadataAfterTransfer.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(jsonMetadataAfterTransfer.description).to.eq(
`@${MOCK_PROFILE_HANDLE} - Lens profile`
);
const expectedAttributesAfterTransfer = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '1' },
{ trait_type: 'owner', value: userAddress.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(jsonMetadataAfterTransfer.attributes).to.eql(expectedAttributesAfterTransfer);
});
it('User should set user two as a dispatcher on their profile, user two should set the profile URI', async function () {
@@ -64,7 +200,10 @@ makeSuiteCleanRoom('Profile URI Functionality', function () {
await expect(
lensHub.connect(userTwo).setProfileImageURI(FIRST_PROFILE_ID, MOCK_URI)
).to.not.be.reverted;
expect(await lensHub.tokenURI(FIRST_PROFILE_ID)).to.eq(MOCK_URI);
const tokenURI = await lensHub.tokenURI(FIRST_PROFILE_ID);
expect(keccak256(toUtf8Bytes(tokenURI))).to.eq(
'0xe1731460db6ce5fa9f3a9f2bd778a8af49e623dceb531df6b1a5c162b7c2d79a'
);
});
it('User should follow profile 1, user should change the follow NFT URI, URI is accurate before and after the change', async function () {
@@ -297,7 +436,11 @@ makeSuiteCleanRoom('Profile URI Functionality', function () {
MAX_UINT256
);
const profileImageURIBefore = await lensHub.tokenURI(FIRST_PROFILE_ID);
const tokenURIBefore = await lensHub.tokenURI(FIRST_PROFILE_ID);
expect(keccak256(toUtf8Bytes(tokenURIBefore))).to.eq(
'0x6ed04aa8ab68b7c5201afb2f9655d8fd483559794ce933b7f2282549ca9e3dba'
);
await expect(
lensHub.setProfileImageURIWithSig({
@@ -312,10 +455,14 @@ makeSuiteCleanRoom('Profile URI Functionality', function () {
})
).to.not.be.reverted;
const profileImageURIAfter = await lensHub.tokenURI(FIRST_PROFILE_ID);
const tokenURIAfter = await lensHub.tokenURI(FIRST_PROFILE_ID);
expect(profileImageURIBefore).to.eq(MOCK_PROFILE_URI);
expect(profileImageURIAfter).to.eq(MOCK_URI);
expect(MOCK_PROFILE_URI).to.not.eq(MOCK_URI);
expect(tokenURIBefore).to.not.eq(tokenURIAfter);
expect(keccak256(toUtf8Bytes(tokenURIAfter))).to.eq(
'0x2f93fe42168b386790c5615061bcd3c1d8aac1976bddca8cf57eb2bc525541ab'
);
});
it('TestWallet should set the follow NFT URI with sig', async function () {

View File

@@ -180,7 +180,7 @@ makeSuiteCleanRoom('Fee Collect Module', function () {
);
await expect(
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
).to.be.revertedWith(ERRORS.ERC20_TRANSFER_EXCEEDS_ALLOWANCE);
).to.be.revertedWith(ERRORS.ERC20_INSUFFICIENT_ALLOWANCE);
});
it('UserTwo should mirror the original post, fail to collect from their mirror without following the original profile', async function () {
@@ -189,7 +189,7 @@ makeSuiteCleanRoom('Fee Collect Module', function () {
lensHub.connect(userTwo).createProfile({
to: userTwoAddress,
handle: 'usertwo',
imageURI: MOCK_PROFILE_URI,
imageURI: MOCK_PROFILE_URI,
followModule: ZERO_ADDRESS,
followModuleData: [],
followNFTURI: MOCK_FOLLOW_NFT_URI,
@@ -217,7 +217,7 @@ makeSuiteCleanRoom('Fee Collect Module', function () {
lensHub.connect(userTwo).createProfile({
to: userTwoAddress,
handle: 'usertwo',
imageURI: MOCK_PROFILE_URI,
imageURI: MOCK_PROFILE_URI,
followModule: ZERO_ADDRESS,
followModuleData: [],
followNFTURI: MOCK_FOLLOW_NFT_URI,
@@ -250,7 +250,7 @@ makeSuiteCleanRoom('Fee Collect Module', function () {
lensHub.connect(userTwo).createProfile({
to: userTwoAddress,
handle: 'usertwo',
imageURI: MOCK_PROFILE_URI,
imageURI: MOCK_PROFILE_URI,
followModule: ZERO_ADDRESS,
followModuleData: [],
followNFTURI: MOCK_FOLLOW_NFT_URI,

View File

@@ -216,7 +216,7 @@ makeSuiteCleanRoom('Limited Fee Collect Module', function () {
);
await expect(
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
).to.be.revertedWith(ERRORS.ERC20_TRANSFER_EXCEEDS_ALLOWANCE);
).to.be.revertedWith(ERRORS.ERC20_INSUFFICIENT_ALLOWANCE);
});
it('UserTwo should mirror the original post, fail to collect from their mirror without following the original profile', async function () {

View File

@@ -231,7 +231,7 @@ makeSuiteCleanRoom('Limited Timed Fee Collect Module', function () {
);
await expect(
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
).to.be.revertedWith(ERRORS.ERC20_TRANSFER_EXCEEDS_ALLOWANCE);
).to.be.revertedWith(ERRORS.ERC20_INSUFFICIENT_ALLOWANCE);
});
it('UserTwo should mirror the original post, fail to collect from their mirror without following the original profile', async function () {
@@ -240,7 +240,7 @@ makeSuiteCleanRoom('Limited Timed Fee Collect Module', function () {
lensHub.connect(userTwo).createProfile({
to: userTwoAddress,
handle: 'usertwo',
imageURI: MOCK_PROFILE_URI,
imageURI: MOCK_PROFILE_URI,
followModule: ZERO_ADDRESS,
followModuleData: [],
followNFTURI: MOCK_FOLLOW_NFT_URI,
@@ -271,7 +271,7 @@ makeSuiteCleanRoom('Limited Timed Fee Collect Module', function () {
lensHub.connect(userTwo).createProfile({
to: userTwoAddress,
handle: 'usertwo',
imageURI: MOCK_PROFILE_URI,
imageURI: MOCK_PROFILE_URI,
followModule: ZERO_ADDRESS,
followModuleData: [],
followNFTURI: MOCK_FOLLOW_NFT_URI,
@@ -307,7 +307,7 @@ makeSuiteCleanRoom('Limited Timed Fee Collect Module', function () {
lensHub.connect(userTwo).createProfile({
to: userTwoAddress,
handle: 'usertwo',
imageURI: MOCK_PROFILE_URI,
imageURI: MOCK_PROFILE_URI,
followModule: ZERO_ADDRESS,
followModuleData: [],
followNFTURI: MOCK_FOLLOW_NFT_URI,
@@ -340,7 +340,7 @@ makeSuiteCleanRoom('Limited Timed Fee Collect Module', function () {
lensHub.connect(userTwo).createProfile({
to: userTwoAddress,
handle: 'usertwo',
imageURI: MOCK_PROFILE_URI,
imageURI: MOCK_PROFILE_URI,
followModule: ZERO_ADDRESS,
followModuleData: [],
followNFTURI: MOCK_FOLLOW_NFT_URI,

View File

@@ -195,7 +195,7 @@ makeSuiteCleanRoom('Timed Fee Collect Module', function () {
);
await expect(
lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data)
).to.be.revertedWith(ERRORS.ERC20_TRANSFER_EXCEEDS_ALLOWANCE);
).to.be.revertedWith(ERRORS.ERC20_INSUFFICIENT_ALLOWANCE);
});
it('UserTwo should mirror the original post, fail to collect from their mirror without following the original profile', async function () {

View File

@@ -139,7 +139,7 @@ makeSuiteCleanRoom('Fee Follow Module', function () {
);
await expect(
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [data])
).to.be.revertedWith(ERRORS.ERC20_TRANSFER_EXCEEDS_ALLOWANCE);
).to.be.revertedWith(ERRORS.ERC20_INSUFFICIENT_ALLOWANCE);
});
});
});

View File

@@ -1,8 +1,10 @@
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 { ERRORS } from '../helpers/errors';
import { getJsonMetadataFromBase64TokenUri } from '../helpers/utils';
import {
approvalFollowModule,
deployer,
@@ -153,7 +155,20 @@ makeSuiteCleanRoom('Misc', function () {
});
it('Profile tokenURI should return the accurate URI', async function () {
expect(await lensHub.tokenURI(FIRST_PROFILE_ID)).to.eq(MOCK_PROFILE_URI);
const tokenURI = await lensHub.tokenURI(FIRST_PROFILE_ID);
const jsonMetadata = await getJsonMetadataFromBase64TokenUri(tokenURI);
expect(jsonMetadata.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(jsonMetadata.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
const expectedAttributes = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '0' },
{ trait_type: 'owner', value: userAddress.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(jsonMetadata.attributes).to.eql(expectedAttributes);
expect(keccak256(toUtf8Bytes(tokenURI))).to.eq(
'0xa2e9967e825705ce8f38931f7d1f88fe63bef6f2f2c52715692f14d42d889b76'
);
});
it('Publication reference module getter should return the correct reference module (or zero in case of no reference module)', async function () {