Lens protocol 是 Aave 团队出品的 SocialFi 项目,我们今天结合它的文档来聊聊它的业务逻辑和合约代码。这篇文章主要着重于对大家看 Lens 代码的一个引导,不会详细解读代码的每一个点,主要是方便大家在看完文章后能对代码更熟悉一些,更快地理解代码。
在大体业务上来说,Lens 的业务逻辑有些类似于 Twitter,主要包括了下面几个方面:
Profile
,一个地址可以拥有多个 Profile,创建一个 Profile 就可以拥有一个对应 NFT,类似于一个人可以拥有多个账户,可以指定默认 Profile
Publication
,发表内容
Comment
,对内容发表评论,也可以对评论发表评论
Mirror
,类似于转发功能
Collect
,将内容铸造为 NFT
Follow
,关注其他 Profile,关注后可以获得 FollowNFT
相对推特来说,主要就是多了 Collect
功能,接下来我们分功能来看看各自的主要逻辑。
用户的主要操作逻辑入口都是在 LensHub
合约中,其中也包含了许多 setter
方法。主要实现逻辑是在 PublishingLogic
和 InteractionLogic
库合约中。
创建 Profile
function createProfile(DataTypes.CreateProfileData calldata vars)
external
override
whenNotPaused
returns (uint256)
{
// 白名单中的用户才能创建
if (!_profileCreatorWhitelisted[msg.sender])
revert Errors.ProfileCreatorNotWhitelisted();
unchecked {
// ++ 表示从 1 开始
uint256 profileId = ++_profileCounter;
_mint(vars.to, profileId);
PublishingLogic.createProfile(
vars,
profileId,
_profileIdByHandleHash,
_profileById,
_followModuleWhitelisted
);
return profileId;
}
}
创建 profile,只有白名单中的地址可以创建,这是为了防止用户名被恶意占领。每次创建一个 profile 会获得一个 NFT。
Profiles can only be minted by addresses that have been whitelisted by governance. This ensures that, given the low-fee environment present on Polygon, the namespace is not reserved by squatters.
我们来看看入参 CreateProfileData
的结构:
struct CreateProfileData {
// 接收 profile 的地址
address to;
// 可以理解为用户名
string handle;
// 头像地址
string imageURI;
address followModule;
bytes followModuleInitData;
string followNFTURI;
}
前面的几个都是一些比较基础的数据字段,后面的几个 follow 字段下文再解释。
下面是创建 profile 的主要逻辑:
function createProfile(
DataTypes.CreateProfileData calldata vars,
uint256 profileId,
mapping(bytes32 => uint256) storage _profileIdByHandleHash,
mapping(uint256 => DataTypes.ProfileStruct) storage _profileById,
mapping(address => bool) storage _followModuleWhitelisted
) external {
// 校验 handle 的内容,只在创建 profile 的时候校验
_validateHandle(vars.handle);
// 要求 imageURI 的长度不能大于限制
if (bytes(vars.imageURI).length > Constants.MAX_PROFILE_IMAGE_URI_LENGTH)
revert Errors.ProfileImageURILengthInvalid();
bytes32 handleHash = keccak256(bytes(vars.handle));
// handle 必须唯一
if (_profileIdByHandleHash[handleHash] != 0) revert Errors.HandleTaken();
// handle hash 对应 profileId
_profileIdByHandleHash[handleHash] = profileId;
// 存储 profileId 对应的数据,handle,imageURI,followNFTURI
_profileById[profileId].handle = vars.handle;
_profileById[profileId].imageURI = vars.imageURI;
_profileById[profileId].followNFTURI = vars.followNFTURI;
bytes memory followModuleReturnData;
if (vars.followModule != address(0)) {
_profileById[profileId].followModule = vars.followModule;
followModuleReturnData = _initFollowModule(
profileId,
vars.followModule,
vars.followModuleInitData,
_followModuleWhitelisted
);
}
_emitProfileCreated(profileId, vars, followModuleReturnData);
}
代码相对也比较好理解,最后的 _initFollowModule
调用我们同样放在后面再讲解,大家记住这里出现过就行。
这里有一个小细节,方法入参中有 mapping
类型,这是只有在 library
库合约中的方法才能实现的,普通的合约方法不能接收 mapping
类型的参数。
发表内容
function post(DataTypes.PostData calldata vars)
external
override
whenPublishingEnabled
returns (uint256)
{
// dispatcher 可以替 owner 为其 post
_validateCallerIsProfileOwnerOrDispatcher(vars.profileId);
return
_createPost(
vars.profileId,
vars.contentURI,
vars.collectModule,
vars.collectModuleInitData,
vars.referenceModule,
vars.referenceModuleInitData
);
}
_validateCallerIsProfileOwnerOrDispatcher
是校验只有用户本人或者用户的 dispatcher 可以调用,类似于 ERC721 中的 operator 角色:
function _validateCallerIsProfileOwnerOrDispatcher(uint256 profileId) internal view {
if (msg.sender == ownerOf(profileId) || msg.sender == _dispatcherByProfile[profileId]) {
return;
}
revert Errors.NotProfileOwnerOrDispatcher();
}
我们来看看入参 PostData
的结构:
struct PostData {
uint256 profileId;
// 存储内容的 URI
string contentURI;
address collectModule;
bytes collectModuleInitData;
address referenceModule;
bytes referenceModuleInitData;
}
后面的几个 collect 和 reference 相关内容我们下文再说。
function _createPost(
uint256 profileId,
string memory contentURI,
address collectModule,
bytes memory collectModuleData,
address referenceModule,
bytes memory referenceModuleData
) internal returns (uint256) {
unchecked {
// 更新发布数量
uint256 pubId = ++_profileById[profileId].pubCount;
PublishingLogic.createPost(
profileId,
contentURI,
collectModule,
collectModuleData,
referenceModule,
referenceModuleData,
pubId,
_pubByIdByProfile,
_collectModuleWhitelisted,
_referenceModuleWhitelisted
);
return pubId;
}
}
_createPost
方法仅仅更新了发布数量就进入了库合约中的逻辑:
function createPost(
uint256 profileId,
string memory contentURI,
address collectModule,
bytes memory collectModuleInitData,
address referenceModule,
bytes memory referenceModuleInitData,
uint256 pubId,
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct))
storage _pubByIdByProfile,
mapping(address => bool) storage _collectModuleWhitelisted,
mapping(address => bool) storage _referenceModuleWhitelisted
) external {
_pubByIdByProfile[profileId][pubId].contentURI = contentURI;
// 新建 post 的时候需要初始化 collet module 和 reference module
// Collect module initialization
bytes memory collectModuleReturnData = _initPubCollectModule(
profileId,
pubId,
collectModule,
collectModuleInitData,
_pubByIdByProfile,
_collectModuleWhitelisted
);
// Reference module initialization
bytes memory referenceModuleReturnData = _initPubReferenceModule(
profileId,
pubId,
referenceModule,
referenceModuleInitData,
_pubByIdByProfile,
_referenceModuleWhitelisted
);
emit Events.PostCreated(
profileId,
pubId,
contentURI,
collectModule,
collectModuleReturnData,
referenceModule,
referenceModuleReturnData,
block.timestamp
);
}
我们来看看 _pubByIdByProfile
的结构:
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct)) internal _pubByIdByProfile;
它的结构是 profileId
→ pubId
→ PublicationStruct
,其中 PublicationStruct
的结构是:
struct PublicationStruct {
uint256 profileIdPointed;
uint256 pubIdPointed;
string contentURI;
address referenceModule;
address collectModule;
address collectNFT;
}
前面两个 pointed 数据代表的是该内容指向的原始内容的 id,只有在当前内容为 Mirror
或者 Comment
类型的时候才有值。
两个 _init
方法我们下文再说。
代码中还有很多的 WithSig
结尾的方法,例如 postWithSig
,这是利用 EIP-712 实现的利用签名可以让其他人代替发送交易的方法,不了解的朋友可以看看我的这篇文章。
发表评论,它的逻辑与 Post 的逻辑很类似,我们主要来看看最核心的部分:
function createComment(
DataTypes.CommentData memory vars,
uint256 pubId,
mapping(uint256 => DataTypes.ProfileStruct) storage _profileById,
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct))
storage _pubByIdByProfile,
mapping(address => bool) storage _collectModuleWhitelisted,
mapping(address => bool) storage _referenceModuleWhitelisted
) external {
// Validate existence of the pointed publication
// 校验传入的数据是否正确
uint256 pubCount = _profileById[vars.profileIdPointed].pubCount;
if (pubCount < vars.pubIdPointed || vars.pubIdPointed == 0)
revert Errors.PublicationDoesNotExist();
// Ensure the pointed publication is not the comment being created
// 不能指向自己的这条 comment
if (vars.profileId == vars.profileIdPointed && vars.pubIdPointed == pubId)
revert Errors.CannotCommentOnSelf();
_pubByIdByProfile[vars.profileId][pubId].contentURI = vars.contentURI;
_pubByIdByProfile[vars.profileId][pubId].profileIdPointed = vars.profileIdPointed;
_pubByIdByProfile[vars.profileId][pubId].pubIdPointed = vars.pubIdPointed;
// Collect Module Initialization
bytes memory collectModuleReturnData = _initPubCollectModule(
vars.profileId,
pubId,
vars.collectModule,
vars.collectModuleInitData,
_pubByIdByProfile,
_collectModuleWhitelisted
);
// Reference module initialization
bytes memory referenceModuleReturnData = _initPubReferenceModule(
vars.profileId,
pubId,
vars.referenceModule,
vars.referenceModuleInitData,
_pubByIdByProfile,
_referenceModuleWhitelisted
);
// Reference module validation
address refModule = _pubByIdByProfile[vars.profileIdPointed][vars.pubIdPointed]
.referenceModule;
if (refModule != address(0)) {
IReferenceModule(refModule).processComment(
vars.profileId,
vars.profileIdPointed,
vars.pubIdPointed,
vars.referenceModuleData
);
}
// Prevents a stack too deep error
_emitCommentCreated(vars, pubId, collectModuleReturnData, referenceModuleReturnData);
}
与 Post 相比,多了指定 profileIdPointed
和 pubIdPointed
的部分,其指向的就是对应的原始内容 Id。还多了 processComment
的调用,我们下文再讲。
入参 CommentData
的结构:
struct CommentData {
uint256 profileId;
string contentURI;
uint256 profileIdPointed;
uint256 pubIdPointed;
bytes referenceModuleData;
address collectModule;
bytes collectModuleInitData;
address referenceModule;
bytes referenceModuleInitData;
}
转发,它的逻辑与前面也是大同小异:
function createMirror(
DataTypes.MirrorData memory vars,
uint256 pubId,
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct))
storage _pubByIdByProfile,
mapping(address => bool) storage _referenceModuleWhitelisted
) external {
// 这里是获取最原始的 id,例如 mirror 另外一个 mirror,这里需要获得最原始的数据
(uint256 rootProfileIdPointed, uint256 rootPubIdPointed, ) = Helpers.getPointedIfMirror(
vars.profileIdPointed,
vars.pubIdPointed,
_pubByIdByProfile
);
_pubByIdByProfile[vars.profileId][pubId].profileIdPointed = rootProfileIdPointed;
_pubByIdByProfile[vars.profileId][pubId].pubIdPointed = rootPubIdPointed;
// Reference module initialization
bytes memory referenceModuleReturnData = _initPubReferenceModule(
vars.profileId,
pubId,
vars.referenceModule,
vars.referenceModuleInitData,
_pubByIdByProfile,
_referenceModuleWhitelisted
);
// Reference module validation
address refModule = _pubByIdByProfile[rootProfileIdPointed][rootPubIdPointed]
.referenceModule;
if (refModule != address(0)) {
IReferenceModule(refModule).processMirror(
vars.profileId,
rootProfileIdPointed,
rootPubIdPointed,
vars.referenceModuleData
);
}
emit Events.MirrorCreated(
vars.profileId,
pubId,
rootProfileIdPointed,
rootPubIdPointed,
vars.referenceModuleData,
vars.referenceModule,
referenceModuleReturnData,
block.timestamp
);
}
多了 getPointedIfMirror
方法,用于获取最原始的 Id,例如 A 文章是原创,B Mirror 了 A,C 又 Mirror 了 B,那么这里不论 Mirror 了多少层,总是获得 A 的 Id。同时后面也多了 processMirror
方法的调用。
入参 MirrorData
的结构:
struct MirrorData {
// 发表在哪个 profile 的名下
uint256 profileId;
// mirror 指向的 profile id
uint256 profileIdPointed;
// mirror 指向的 pub id
uint256 pubIdPointed;
bytes referenceModuleData;
address referenceModule;
bytes referenceModuleInitData;
}
Collect 是将内容铸造为 NFT 的过程,例如用户认为某个内容很好,想将其铸造为 NFT,可以类比为将喜爱的照片装裱的过程。
来看看代码:
function collect(
address collector,
uint256 profileId,
uint256 pubId,
bytes calldata collectModuleData,
address collectNFTImpl,
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct))
storage _pubByIdByProfile,
mapping(uint256 => DataTypes.ProfileStruct) storage _profileById
) external returns (uint256) {
// 这里是获取最原始的 pub 数据,包括 profileId, pubId,collectModule
(uint256 rootProfileId, uint256 rootPubId, address rootCollectModule) = Helpers
.getPointedIfMirror(profileId, pubId, _pubByIdByProfile);
uint256 tokenId;
// Avoids stack too deep
{
address collectNFT = _pubByIdByProfile[rootProfileId][rootPubId].collectNFT;
if (collectNFT == address(0)) {
// 如果是第一次 collect,则部署一个新的 collect
collectNFT = _deployCollectNFT(
rootProfileId,
rootPubId,
_profileById[rootProfileId].handle,
collectNFTImpl
);
_pubByIdByProfile[rootProfileId][rootPubId].collectNFT = collectNFT;
}
// 第一次 collect 需要部署合约,后面的不需要,直接 mint 就行
tokenId = ICollectNFT(collectNFT).mint(collector);
}
ICollectModule(rootCollectModule).processCollect(
profileId,
collector,
rootProfileId,
rootPubId,
collectModuleData
);
_emitCollectedEvent(
collector,
profileId,
pubId,
rootProfileId,
rootPubId,
collectModuleData
);
return tokenId;
}
既然 collect 会铸造 NFT,那么肯定就有 mint NFT 的过程。我们在上面代码中看到,从 _pubByIdByProfile
中获取 collectNFT
的地址,如果为空,说明该内容是第一次被 collect,此时需要部署一个 collectNFT
的合约,如果不为空,则直接 mint 即可。部署合约这块运用了 EIP-1167 的内容,可以节省 Gas 费用,不了解的朋友可以看看我的这篇文章。
processCollect
是执行 collect 的一些逻辑。我们前面涉及到 collectModule
的部分一直没讲,现在来看看这一块究竟是什么逻辑。实际上 collectModule
就是对 collect 过程的个性化定制模块。例如,内容创造者要求最多只能 collect 100 份,或者是他想要对 collect 进行收费,其他用户必须缴纳一定费用才能进行 collect。collectModule
就是用来实现这些多种多样的功能。目前 Lens 官方配置了下面几种 collect 配置:
FreeCollectModule
,免费 collect
FeeCollectModule
,用户需要支付一定量的费用(Token)才能 collect
TimedFeeCollectModule
,用户只能在内容发布后的一段时间内进行 collect,且需要支付费用
LimitedFeeCollectModule
,用户需要支付费用,并且有数量上限,例如最多只能 collect 500份
LimitedTimedFeeCollectModule
,用户需要支付费用,并且有数量上限,同时也有时间限制
RevertCollectModule
,任何情况下都不能 collect,否则交易失败
上面这几个,除了最后一个 RevertCollectModule
,其他的都有一个先决条件是先判断是否只有 follower 才能 collect,这个配置是在用户创建 profile 的时候配置的,也就是说用户可以设置是否只有 follower 才能 collect,这个设置是在 followModule
中。
collectModule
主要有两个模块,init
和 process
。init
会初始化一些参数,例如如果用户使用的是 FeeCollectModule
,那么 init
就会根据用户传入的参数指定 Token 的类型以及数量等,process
是执行 collect 时进行的逻辑,在这里就会执行转账操作。我们前面看到的 _initPubCollectModule
方法就属于 init
部分,用户会在创建 post 或者 comment 的时候调用 init
,在 collect 的时候执行 process
。
Follow 顾名思义就是关注其他 profile,与 collect 相同的是 follow 时也会铸造 NFT,类似于粉丝拥有了一个明星的徽章。
function follow(
address follower,
uint256[] calldata profileIds,
bytes[] calldata followModuleDatas,
mapping(uint256 => DataTypes.ProfileStruct) storage _profileById,
mapping(bytes32 => uint256) storage _profileIdByHandleHash
) external returns (uint256[] memory) {
if (profileIds.length != followModuleDatas.length) revert Errors.ArrayMismatch();
uint256[] memory tokenIds = new uint256[](profileIds.length);
for (uint256 i = 0; i < profileIds.length; ) {
string memory handle = _profileById[profileIds[i]].handle;
if (_profileIdByHandleHash[keccak256(bytes(handle))] != profileIds[i])
revert Errors.TokenDoesNotExist();
address followModule = _profileById[profileIds[i]].followModule;
address followNFT = _profileById[profileIds[i]].followNFT;
if (followNFT == address(0)) {
followNFT = _deployFollowNFT(profileIds[i]);
_profileById[profileIds[i]].followNFT = followNFT;
}
tokenIds[i] = IFollowNFT(followNFT).mint(follower);
if (followModule != address(0)) {
IFollowModule(followModule).processFollow(
follower,
profileIds[i],
followModuleDatas[i]
);
}
unchecked {
++i;
}
}
emit Events.Followed(follower, profileIds, followModuleDatas, block.timestamp);
return tokenIds;
}
follow 方法支持批量 follow,因此接收的是一个 profileIds
的数组,遍历这一个数组,逐个进行 follow。同样的,如果是该 profile 第一次被 follow,则需要部署 NFT 合约,否则直接 mint 即可。
follow 与 collect 同样都支持定制化模块,follow 目前支持的模块如下:
ProfileFollowModule
,正常 follow,没有限制
ApprovalFollowModule
,只有授权过的地址可以 follow
FeeFollowModule
,follow 需要付费
RevertFollowModule
,不允许 follow,否则直接失败
followModule
同样包含 init
和 process
两个模块。用户可以在创建 profile 的时候调用 init
,即 _initFollowModule
,也可以后期修改。在 follow 的时候执行 process
。
其实 reference
不算是一个额外的功能,它指的是 comment
或者 mirror
操作。但是它也有自己对应的 module,所以这里把它单独拿出来讲讲。在用户创建 post
,comment
,mirror
的时候,可以指定当前创建的这个新 pub 在被 reference
时的要求。目前只支持一个模块:
FollowerOnlyReferenceModule
,只有 follower 可以进行 reference
referenceModule
也是包含 init
和 process
两个模块。在用户创建 post
,comment
,mirror
的时候,会调用 init
,即 _initPubReferenceModule
。在用户进行 comment
,mirror
的时候会调用 process
。注意前后两个 comment
,mirror
对应的不是一个对象,init
时是指定当该 pub 被 reference
时的要求,process
时是执行当前 reference
对象的要求。
我们已经将 Lens 的主要逻辑代码看完,整体来说不算难,业务逻辑相对也比较简单,没有特别绕的逻辑。希望能对大家起一个代码导读的作用,接下来看代码会更加容易。
欢迎和我交流