# Lens protocol 合约代码浅析

By [xyyme.eth](https://paragraph.com/@xyyme) · 2022-11-25

---

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 实现的利用签名可以让其他人代替发送交易的方法，不了解的朋友可以看看我的这篇[文章](https://mirror.xyz/xyyme.eth/cJX3zqiiUg2dxB1nmbXbDcQ1DSdajHP5iNgBc6wEZz4)。

### 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 费用，不了解的朋友可以看看我的这篇[文章](https://mirror.xyz/xyyme.eth/mmUAYWFLfcHGCEFg8903SweY3Sl-xIACZNDXOJ3twz8)。

`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 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`，只有授权过的地址可以 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 的主要逻辑代码看完，整体来说不算难，业务逻辑相对也比较简单，没有特别绕的逻辑。希望能对大家起一个代码导读的作用，接下来看代码会更加容易。

关于我
---

欢迎[和我交流](https://linktr.ee/xyymeeth)

参考
--

[

Lens Chain
----------

Scalable 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

![](https://storage.googleapis.com/papyrus_images/d15b3ff39578c26e897f2f4b9be2ff4ce689c553ea2f14d405fd67bc552bb616.png)

](https://docs.lens.xyz/docs)

---

*Originally published on [xyyme.eth](https://paragraph.com/@xyyme/lens-protocol)*
