Files
core/contracts/core/LensHub.sol
Peter Michael dd137b2dee Initial commit
2022-01-25 15:19:42 -05:00

960 lines
30 KiB
Solidity

// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.10;
import {ILensHub} from '../interfaces/ILensHub.sol';
import {Events} from '../libraries/Events.sol';
import {Helpers} from '../libraries/Helpers.sol';
import {DataTypes} from '../libraries/DataTypes.sol';
import {Errors} from '../libraries/Errors.sol';
import {PublishingLogic} from '../libraries/PublishingLogic.sol';
import {InteractionLogic} from '../libraries/InteractionLogic.sol';
import {LensNFTBase} from './base/LensNFTBase.sol';
import {LensMultiState} from './base/LensMultiState.sol';
import {LensHubStorage} from './storage/LensHubStorage.sol';
import {VersionedInitializable} from '../upgradeability/VersionedInitializable.sol';
/**
* @title LensHub
* @author Lens
*
* @notice This is the main entrypoint of the Lens protocol. It contains governance functionality as well as
* publishing and profile interaction functionality.
*
* NOTE: The Lens protocol is unique in that frontend operators need to track a potentially overwhelming
* number of NFT contracts and interactions at once. For that reason, we've made two quirky design decisions:
* 1. Both Follow & Collect NFTs invoke an LensHub callback on transfer with the sole purpose of emitting an event.
* 2. Almost every event in the protocol emits the current block timestamp, reducing the need to fetch it manually.
*/
contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiState, LensHubStorage {
uint256 internal constant REVISION = 1;
address internal immutable FOLLOW_NFT_IMPL;
address internal immutable COLLECT_NFT_IMPL;
/**
* @dev This modifier reverts if the caller is not the configured governance address.
*/
modifier onlyGov() {
_validateCallerIsGovernance();
_;
}
/**
* @dev This modifier reverts if the caller is not a whitelisted profile creator address.
*/
modifier onlyWhitelistedProfileCreator() {
_validateCallerIsWhitelistedProfileCreator();
_;
}
/**
* @dev The constructor sets the immutable follow & collect NFT implementations.
*
* @param followNFTImpl The follow NFT implementation address.
* @param collectNFTImpl The collect NFT implementation address.
*/
constructor(address followNFTImpl, address collectNFTImpl) {
FOLLOW_NFT_IMPL = followNFTImpl;
COLLECT_NFT_IMPL = collectNFTImpl;
}
/// @inheritdoc ILensHub
function initialize(
string calldata name,
string calldata symbol,
address newGovernance
) external override initializer {
super._initialize(name, symbol);
_setState(DataTypes.ProtocolState.Paused);
_setGovernance(newGovernance);
}
/// ***********************
/// *****GOV FUNCTIONS*****
/// ***********************
/// @inheritdoc ILensHub
function setGovernance(address newGovernance) external override onlyGov {
_setGovernance(newGovernance);
}
/// @inheritdoc ILensHub
function setEmergencyAdmin(address newEmergencyAdmin) external override onlyGov {
address prevEmergencyAdmin = _emergencyAdmin;
_emergencyAdmin = newEmergencyAdmin;
emit Events.EmergencyAdminSet(
msg.sender,
prevEmergencyAdmin,
newEmergencyAdmin,
block.timestamp
);
}
/// @inheritdoc ILensHub
function setState(DataTypes.ProtocolState newState) external override {
if (msg.sender != _governance && msg.sender != _emergencyAdmin)
revert Errors.NotGovernanceOrEmergencyAdmin();
_setState(newState);
}
///@inheritdoc ILensHub
function whitelistProfileCreator(address profileCreator, bool whitelist)
external
override
onlyGov
{
_profileCreatorWhitelisted[profileCreator] = whitelist;
emit Events.ProfileCreatorWhitelisted(profileCreator, whitelist, block.timestamp);
}
/// @inheritdoc ILensHub
function whitelistFollowModule(address followModule, bool whitelist) external override onlyGov {
_followModuleWhitelisted[followModule] = whitelist;
emit Events.FollowModuleWhitelisted(followModule, whitelist, block.timestamp);
}
/// @inheritdoc ILensHub
function whitelistReferenceModule(address referenceModule, bool whitelist)
external
override
onlyGov
{
_referenceModuleWhitelisted[referenceModule] = whitelist;
emit Events.ReferenceModuleWhitelisted(referenceModule, whitelist, block.timestamp);
}
/// @inheritdoc ILensHub
function whitelistCollectModule(address collectModule, bool whitelist)
external
override
onlyGov
{
_collectModuleWhitelisted[collectModule] = whitelist;
emit Events.CollectModuleWhitelisted(collectModule, whitelist, block.timestamp);
}
/// *********************************
/// *****PROFILE OWNER FUNCTIONS*****
/// *********************************
/// @inheritdoc ILensHub
function createProfile(DataTypes.CreateProfileData calldata vars)
external
override
whenNotPaused
onlyWhitelistedProfileCreator
{
uint256 profileId = ++_profileCounter;
_mint(vars.to, profileId);
PublishingLogic.createProfile(
vars,
profileId,
_profileIdByHandleHash,
_profileById,
_followModuleWhitelisted
);
}
/// @inheritdoc ILensHub
function setFollowModule(
uint256 profileId,
address followModule,
bytes calldata followModuleData
) external override whenNotPaused {
_validateCallerIsProfileOwner(profileId);
PublishingLogic.setFollowModule(
profileId,
followModule,
followModuleData,
_profileById[profileId],
_followModuleWhitelisted
);
}
/// @inheritdoc ILensHub
function setFollowModuleWithSig(DataTypes.SetFollowModuleWithSigData calldata vars)
external
override
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(digest, owner, vars.sig);
PublishingLogic.setFollowModule(
vars.profileId,
vars.followModule,
vars.followModuleData,
_profileById[vars.profileId],
_followModuleWhitelisted
);
}
/// @inheritdoc ILensHub
function setDispatcher(uint256 profileId, address dispatcher) external override whenNotPaused {
_validateCallerIsProfileOwner(profileId);
_setDispatcher(profileId, dispatcher);
}
/// @inheritdoc ILensHub
function setDispatcherWithSig(DataTypes.SetDispatcherWithSigData calldata vars)
external
override
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(digest, owner, vars.sig);
_setDispatcher(vars.profileId, vars.dispatcher);
}
/// @inheritdoc ILensHub
function setProfileImageURI(uint256 profileId, string calldata imageURI)
external
override
whenNotPaused
{
_validateCallerIsProfileOwnerOrDispatcher(profileId);
_setProfileImageURI(profileId, imageURI);
}
/// @inheritdoc ILensHub
function setProfileImageURIWithSig(DataTypes.SetProfileImageURIWithSigData calldata vars)
external
override
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(digest, owner, vars.sig);
_setProfileImageURI(vars.profileId, vars.imageURI);
}
/// @inheritdoc ILensHub
function setFollowNFTURI(uint256 profileId, string calldata followNFTURI)
external
override
whenNotPaused
{
_validateCallerIsProfileOwnerOrDispatcher(profileId);
_setFollowNFTURI(profileId, followNFTURI);
}
/// @inheritdoc ILensHub
function setFollowNFTURIWithSig(DataTypes.SetFollowNFTURIWithSigData calldata vars)
external
override
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(digest, owner, vars.sig);
}
/// @inheritdoc ILensHub
function post(DataTypes.PostData calldata vars) external override whenPublishingEnabled {
_validateCallerIsProfileOwnerOrDispatcher(vars.profileId);
_createPost(
vars.profileId,
vars.contentURI,
vars.collectModule,
vars.collectModuleData,
vars.referenceModule,
vars.referenceModuleData
);
}
/// @inheritdoc ILensHub
function postWithSig(DataTypes.PostWithSigData calldata vars)
external
override
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(digest, owner, vars.sig);
_createPost(
vars.profileId,
vars.contentURI,
vars.collectModule,
vars.collectModuleData,
vars.referenceModule,
vars.referenceModuleData
);
}
/// @inheritdoc ILensHub
function comment(DataTypes.CommentData calldata vars) external override whenPublishingEnabled {
_validateCallerIsProfileOwnerOrDispatcher(vars.profileId);
_createComment(vars);
}
/// @inheritdoc ILensHub
function commentWithSig(DataTypes.CommentWithSigData calldata vars)
external
override
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(digest, owner, vars.sig);
_createComment(
DataTypes.CommentData(
vars.profileId,
vars.contentURI,
vars.profileIdPointed,
vars.pubIdPointed,
vars.collectModule,
vars.collectModuleData,
vars.referenceModule,
vars.referenceModuleData
)
);
}
/// @inheritdoc ILensHub
function mirror(DataTypes.MirrorData calldata vars) external override whenPublishingEnabled {
_validateCallerIsProfileOwnerOrDispatcher(vars.profileId);
_createMirror(
vars.profileId,
vars.profileIdPointed,
vars.pubIdPointed,
vars.referenceModule,
vars.referenceModuleData
);
}
/// @inheritdoc ILensHub
function mirrorWithSig(DataTypes.MirrorWithSigData calldata vars)
external
override
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(digest, owner, vars.sig);
_createMirror(
vars.profileId,
vars.profileIdPointed,
vars.pubIdPointed,
vars.referenceModule,
vars.referenceModuleData
);
}
/**
* @notice Burns a profile, this maintains the profile data struct, but deletes the
* handle hash to profile ID mapping value.
*
* NOTE: This overrides the LensNFTBase contract's `burn()` function and calls it to fully burn
* the NFT.
*/
function burn(uint256 tokenId) public override whenNotPaused {
super.burn(tokenId);
_clearHandleHash(tokenId);
}
/**
* @notice Burns a profile with a signature, this maintains the profile data struct, but deletes the
* handle hash to profile ID mapping value.
*
* NOTE: This overrides the LensNFTBase contract's `burnWithSig()` function and calls it to fully burn
* the NFT.
*/
function burnWithSig(uint256 tokenId, DataTypes.EIP712Signature calldata sig)
public
override
whenNotPaused
{
super.burnWithSig(tokenId, sig);
_clearHandleHash(tokenId);
}
/// ***************************************
/// *****PROFILE INTERACTION FUNCTIONS*****
/// ***************************************
/// @inheritdoc ILensHub
function follow(uint256[] calldata profileIds, bytes[] calldata datas)
external
override
whenNotPaused
{
InteractionLogic.follow(
msg.sender,
profileIds,
datas,
FOLLOW_NFT_IMPL,
_profileById,
_profileIdByHandleHash
);
}
/// @inheritdoc ILensHub
function followWithSig(DataTypes.FollowWithSigData calldata vars)
external
override
whenNotPaused
{
bytes32[] memory dataHashes = new bytes32[](vars.datas.length);
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(digest, vars.follower, vars.sig);
InteractionLogic.follow(
vars.follower,
vars.profileIds,
vars.datas,
FOLLOW_NFT_IMPL,
_profileById,
_profileIdByHandleHash
);
}
/// @inheritdoc ILensHub
function collect(
uint256 profileId,
uint256 pubId,
bytes calldata data
) external override whenNotPaused {
InteractionLogic.collect(
msg.sender,
profileId,
pubId,
data,
COLLECT_NFT_IMPL,
_pubByIdByProfile,
_profileById
);
}
/// @inheritdoc ILensHub
function collectWithSig(DataTypes.CollectWithSigData calldata vars)
external
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(digest, vars.collector, vars.sig);
InteractionLogic.collect(
vars.collector,
vars.profileId,
vars.pubId,
vars.data,
COLLECT_NFT_IMPL,
_pubByIdByProfile,
_profileById
);
}
/// @inheritdoc ILensHub
function emitFollowNFTTransferEvent(
uint256 profileId,
uint256 followNFTId,
address from,
address to
) external override {
address expectedFollowNFT = _profileById[profileId].followNFT;
if (msg.sender != expectedFollowNFT) revert Errors.CallerNotFollowNFT();
emit Events.FollowNFTTransferred(profileId, followNFTId, from, to, block.timestamp);
}
/// @inheritdoc ILensHub
function emitCollectNFTTransferEvent(
uint256 profileId,
uint256 pubId,
uint256 collectNFTId,
address from,
address to
) external override {
address expectedCollectNFT = _pubByIdByProfile[profileId][pubId].collectNFT;
if (msg.sender != expectedCollectNFT) revert Errors.CallerNotCollectNFT();
emit Events.CollectNFTTransferred(
profileId,
pubId,
collectNFTId,
from,
to,
block.timestamp
);
}
/// *********************************
/// *****EXTERNAL VIEW FUNCTIONS*****
/// *********************************
/// @inheritdoc ILensHub
function isProfileCreatorWhitelisted(address profileCreator)
external
view
override
returns (bool)
{
return _profileCreatorWhitelisted[profileCreator];
}
/// @inheritdoc ILensHub
function isFollowModuleWhitelisted(address followModule) external view override returns (bool) {
return _followModuleWhitelisted[followModule];
}
/// @inheritdoc ILensHub
function isReferenceModuleWhitelisted(address referenceModule)
external
view
override
returns (bool)
{
return _referenceModuleWhitelisted[referenceModule];
}
/// @inheritdoc ILensHub
function isCollectModuleWhitelisted(address collectModule)
external
view
override
returns (bool)
{
return _collectModuleWhitelisted[collectModule];
}
/// @inheritdoc ILensHub
function getGovernance() external view override returns (address) {
return _governance;
}
/// @inheritdoc ILensHub
function getDispatcher(uint256 profileId) external view override returns (address) {
return _dispatcherByProfile[profileId];
}
/// @inheritdoc ILensHub
function getPubCount(uint256 profileId) external view override returns (uint256) {
return _profileById[profileId].pubCount;
}
/// @inheritdoc ILensHub
function getFollowNFT(uint256 profileId) external view override returns (address) {
return _profileById[profileId].followNFT;
}
/// @inheritdoc ILensHub
function getFollowNFTURI(uint256 profileId) external view override returns (string memory) {
return _profileById[profileId].followNFTURI;
}
/// @inheritdoc ILensHub
function getCollectNFT(uint256 profileId, uint256 pubId)
external
view
override
returns (address)
{
return _pubByIdByProfile[profileId][pubId].collectNFT;
}
/// @inheritdoc ILensHub
function getFollowModule(uint256 profileId) external view override returns (address) {
return _profileById[profileId].followModule;
}
/// @inheritdoc ILensHub
function getCollectModule(uint256 profileId, uint256 pubId)
external
view
override
returns (address)
{
return _pubByIdByProfile[profileId][pubId].collectModule;
}
/// @inheritdoc ILensHub
function getReferenceModule(uint256 profileId, uint256 pubId)
external
view
override
returns (address)
{
return _pubByIdByProfile[profileId][pubId].referenceModule;
}
/// @inheritdoc ILensHub
function getHandle(uint256 profileId) external view override returns (string memory) {
return _profileById[profileId].handle;
}
/// @inheritdoc ILensHub
function getPubPointer(uint256 profileId, uint256 pubId)
external
view
override
returns (uint256, uint256)
{
uint256 profileIdPointed = _pubByIdByProfile[profileId][pubId].profileIdPointed;
uint256 pubIdPointed = _pubByIdByProfile[profileId][pubId].pubIdPointed;
return (profileIdPointed, pubIdPointed);
}
/// @inheritdoc ILensHub
function getContentURI(uint256 profileId, uint256 pubId)
external
view
override
returns (string memory)
{
(uint256 rootProfileId, uint256 rootPubId, ) = Helpers.getPointedIfMirror(
profileId,
pubId,
_pubByIdByProfile
);
return _pubByIdByProfile[rootProfileId][rootPubId].contentURI;
}
/// @inheritdoc ILensHub
function getProfileIdByHandle(string calldata handle) external view override returns (uint256) {
bytes32 handleHash = keccak256(bytes(handle));
return _profileIdByHandleHash[handleHash];
}
/// @inheritdoc ILensHub
function getProfile(uint256 profileId)
external
view
override
returns (DataTypes.ProfileStruct memory)
{
return _profileById[profileId];
}
/// @inheritdoc ILensHub
function getPub(uint256 profileId, uint256 pubId)
external
view
override
returns (DataTypes.PublicationStruct memory)
{
return _pubByIdByProfile[profileId][pubId];
}
/// @inheritdoc ILensHub
function getPubType(uint256 profileId, uint256 pubId)
external
view
override
returns (DataTypes.PubType)
{
if (pubId == 0 || _profileById[profileId].pubCount < pubId) {
return DataTypes.PubType.Nonexistent;
} else if (_pubByIdByProfile[profileId][pubId].collectModule == address(0)) {
return DataTypes.PubType.Mirror;
} else {
if (_pubByIdByProfile[profileId][pubId].profileIdPointed == 0) {
return DataTypes.PubType.Post;
} else {
return DataTypes.PubType.Comment;
}
}
}
/**
* @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
}
/// ****************************
/// *****INTERNAL FUNCTIONS*****
/// ****************************
function _setGovernance(address newGovernance) internal {
address prevGovernance = _governance;
_governance = newGovernance;
emit Events.GovernanceSet(msg.sender, prevGovernance, newGovernance, block.timestamp);
}
function _createPost(
uint256 profileId,
string memory contentURI,
address collectModule,
bytes memory collectModuleData,
address referenceModule,
bytes memory referenceModuleData
) internal {
PublishingLogic.createPost(
profileId,
contentURI,
collectModule,
collectModuleData,
referenceModule,
referenceModuleData,
++_profileById[profileId].pubCount,
_pubByIdByProfile,
_collectModuleWhitelisted,
_referenceModuleWhitelisted
);
}
function _createComment(DataTypes.CommentData memory vars) internal {
PublishingLogic.createComment(
vars,
_profileById[vars.profileId].pubCount + 1,
_profileById,
_pubByIdByProfile,
_collectModuleWhitelisted,
_referenceModuleWhitelisted
);
_profileById[vars.profileId].pubCount++;
}
function _createMirror(
uint256 profileId,
uint256 profileIdPointed,
uint256 pubIdPointed,
address referenceModule,
bytes calldata referenceModuleData
) internal {
PublishingLogic.createMirror(
profileId,
profileIdPointed,
pubIdPointed,
referenceModule,
referenceModuleData,
++_profileById[profileId].pubCount,
_pubByIdByProfile,
_referenceModuleWhitelisted
);
}
function _setDispatcher(uint256 profileId, address dispatcher) internal {
_dispatcherByProfile[profileId] = dispatcher;
emit Events.DispatcherSet(profileId, dispatcher, block.timestamp);
}
function _setProfileImageURI(uint256 profileId, string memory imageURI) internal {
_profileById[profileId].imageURI = imageURI;
emit Events.ProfileImageURISet(profileId, imageURI, block.timestamp);
}
function _setFollowNFTURI(uint256 profileId, string memory followNFTURI) internal {
_profileById[profileId].followNFTURI = followNFTURI;
emit Events.FollowNFTURISet(profileId, followNFTURI, block.timestamp);
}
function _clearHandleHash(uint256 profileId) internal {
bytes32 handleHash = keccak256(bytes(_profileById[profileId].handle));
_profileIdByHandleHash[handleHash] = 0;
}
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal override {
if (_dispatcherByProfile[tokenId] != address(0)) {
_setDispatcher(tokenId, address(0));
}
super._beforeTokenTransfer(from, to, tokenId);
}
function _validateCallerIsProfileOwnerOrDispatcher(uint256 profileId) internal view {
if (msg.sender != ownerOf(profileId) && msg.sender != _dispatcherByProfile[profileId])
revert Errors.NotProfileOwnerOrDispatcher();
}
function _validateCallerIsProfileOwner(uint256 profileId) internal view {
if (msg.sender != ownerOf(profileId)) revert Errors.NotProfileOwner();
}
function _validateCallerIsGovernance() internal view {
if (msg.sender != _governance) revert Errors.NotGovernance();
}
function _validateCallerIsWhitelistedProfileCreator() internal view {
if (!_profileCreatorWhitelisted[msg.sender]) revert Errors.ProfileCreatorNotWhitelisted();
}
function getRevision() internal pure virtual override returns (uint256) {
return REVISION;
}
}