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
创建 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
类型的参数。
Publication
发表内容
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 实现的利用签名可以让其他人代替发送交易的方法,不了解的朋友可以看看我的这篇文章。
Comment
发表评论,它的逻辑与 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;
}
Mirror
转发,它的逻辑与前面也是大同小异:
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
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
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
其实 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 的主要逻辑代码看完,整体来说不算难,业务逻辑相对也比较简单,没有特别绕的逻辑。希望能对大家起一个代码导读的作用,接下来看代码会更加容易。
关于我
欢迎和我交流