# Lens protocol 合约代码浅析 **Published by:** [xyyme.eth](https://paragraph.com/@xyyme/) **Published on:** 2022-11-25 **URL:** https://paragraph.com/@xyyme/lens-protocol ## Content Lens protocol 是 Aave 团队出品的 SocialFi 项目,我们今天结合它的文档来聊聊它的业务逻辑和合约代码。这篇文章主要着重于对大家看 Lens 代码的一个引导,不会详细解读代码的每一个点,主要是方便大家在看完文章后能对代码更熟悉一些,更快地理解代码。代码分析在大体业务上来说,Lens 的业务逻辑有些类似于 Twitter,主要包括了下面几个方面:Profile,一个地址可以拥有多个 Profile,创建一个 Profile 就可以拥有一个对应 NFT,类似于一个人可以拥有多个账户,可以指定默认 ProfilePublication,发表内容Comment,对内容发表评论,也可以对评论发表评论Mirror,类似于转发功能Collect,将内容铸造为 NFTFollow,关注其他 Profile,关注后可以获得 FollowNFT相对推特来说,主要就是多了 Collect 功能,接下来我们分功能来看看各自的主要逻辑。 用户的主要操作逻辑入口都是在 LensHub 合约中,其中也包含了许多 setter 方法。主要实现逻辑是在 PublishingLogic 和 InteractionLogic 库合约中。Profile创建 Profilefunction 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; } CollectCollect 是将内容铸造为 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,免费 collectFeeCollectModule,用户需要支付一定量的费用(Token)才能 collectTimedFeeCollectModule,用户只能在内容发布后的一段时间内进行 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。FollowFollow 顾名思义就是关注其他 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 uint256Unsupported embed; 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,只有授权过的地址可以 followFeeFollowModule,follow 需要付费RevertFollowModule,不允许 follow,否则直接失败followModule 同样包含 init 和 process 两个模块。用户可以在创建 profile 的时候调用 init,即 _initFollowModule,也可以后期修改。在 follow 的时候执行 process。Reference其实 reference 不算是一个额外的功能,它指的是 comment 或者 mirror 操作。但是它也有自己对应的 module,所以这里把它单独拿出来讲讲。在用户创建 post,comment,mirror 的时候,可以指定当前创建的这个新 pub 在被 reference 时的要求。目前只支持一个模块:FollowerOnlyReferenceModule,只有 follower 可以进行 referencereferenceModule 也是包含 init 和 process 两个模块。在用户创建 post,comment,mirror 的时候,会调用 init,即 _initPubReferenceModule。在用户进行 comment,mirror 的时候会调用 process。注意前后两个 comment,mirror 对应的不是一个对象,init 时是指定当该 pub 被 reference 时的要求,process 时是执行当前 reference 对象的要求。总结我们已经将 Lens 的主要逻辑代码看完,整体来说不算难,业务逻辑相对也比较简单,没有特别绕的逻辑。希望能对大家起一个代码导读的作用,接下来看代码会更加容易。关于我欢迎和我交流参考Lens ChainScalable SocialFi on Ethereum. Lens is a high-performance blockchain stack built for SocialFi, combining modular Social Primitives, fast settlement, and decentralized storage. The Lens stack is composed of Lens Chain, Social Protocol, and Storage Nodes. As a layer 2 leveraging ZKsync, Avail, and Ethereum's security, Lens delivers the fastest, most cost-effective, and scalable blockchain for developers.https://lens.xyz ## Publication Information - [xyyme.eth](https://paragraph.com/@xyyme/): Publication homepage - [All Posts](https://paragraph.com/@xyyme/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@xyyme): Subscribe to updates - [Twitter](https://twitter.com/xyymeeth): Follow on Twitter