EIP-712 使用详解
之前的文章我们介绍过如何对数据进行签名,利用签名技术我们可以实现一些功能例如白名单校验等。但是这种签名技术的应用场景比较简单,一般就是给一串字符串,或者一串哈希签名,如果我们想为更复杂的数据签名就无法实现了。 EIP-712 的出现就是为了解决这个问题,利用 EIP-712,我们可以对更大的数据集,例如对结构体进行签名。那么这种签名格式有什么实际的应用场景呢。使用过 Uniswap,PancakeSwap 等 DEX 的朋友应该有印象,在移除 LP 流动性的时候,我们需要先签名,然后再发送一笔交易移除流动性。正常情况下,其实应该我们先调用 LP 代币的授权方法,授权 DEX 合约可以转移我们的 LP,然后再去移除流动性。而这种二合一的实现正是应用了 EIP-712。它帮助我们仅仅签名一次,就可以将两步交易合并为一步交易,从而节省 Gas 费用。这篇文章我们就来看看 EIP-712 到底是怎么使用的。基本结构EIP712Domain顾名思义,是一个与域相关的结构体,总共包含五个字段:name,合约或者协议的名称version,合约的版本chainId,合约部署的链 Id,一般使用 ...
流动性挖矿-合约原理详解
流动性挖矿应该是上个牛市最火热的内容,基本上整个 DeFi 都是在围绕着流动性挖矿展开的,今天我们就来看看它到底是什么以及合约代码层面是怎么实现的。流动性挖矿简介首先我们先从用户的角度来理解一下流动性挖矿是什么,实际上就是用户通过在合约中质押一个 token 从而赚取另一个 token 的过程。例如,SushiSwap 最初推出的 DEX 流动性挖矿,用户可以通过将 SushiSwap 的 LP token 质押到合约中赚取 Sushi token。那么这个奖励具体是怎么发放以及如何实现的呢?我们今天就来研究一下这部分内容。 先来看几个例子: 一:假设有一个流动性挖矿的合约,可以质押 A token 赚取 B token。它在 0 秒时开始活动,每秒奖励 R 个 B token。此时有用户 Alice 在第 3 秒时质押了 2 个 A token,并且之后没有其他人参与,在第 8 秒时取出 token,图示:那么他在此时获得的收益就是:5R = (2 / 2) * (8 - 3) * R 其中,第一个 2 是用户 A 质押的数量,第二个 2 是合约中质押的总量,(8-3)是用户 ...
CREATE2 操作码使用方法详解
CREATE2 是一个可以在合约中创建合约的操作码。我们先来举个例子看看它能干什么:这段代码是 Uniswap v2-core 里面的工厂合约代码,使用 create2 操作码创建了 pair 合约,返回值是 pair 的地址,这样就可以逻辑中直接使用其地址进行接下来的操作。 那么 create2 到底是怎么使用呢,根据官方 EIP 文档,create2 一共接收四个参数,分别是:endowment(创建合约时往合约中打的 ETH 数量)memory_start(代码在内存中的起始位置,一般固定为 add(bytecode, 0x20) )memory_length(代码长度,一般固定为 mload(bytecode) )salt(随机数盐)这里要注意的是第一个参数如果大于 0 的话,需要待部署合约的构造方法带有 payable。随机数盐是由用户自定,须为 bytes32 格式,例如在上面 Uniswap 的例子中,salt 为:bytes32 salt = keccak256(abi.encodePacked(token0, token1)); create2 还有一个优点,相...
Smart Contract Developer
EIP-712 使用详解
之前的文章我们介绍过如何对数据进行签名,利用签名技术我们可以实现一些功能例如白名单校验等。但是这种签名技术的应用场景比较简单,一般就是给一串字符串,或者一串哈希签名,如果我们想为更复杂的数据签名就无法实现了。 EIP-712 的出现就是为了解决这个问题,利用 EIP-712,我们可以对更大的数据集,例如对结构体进行签名。那么这种签名格式有什么实际的应用场景呢。使用过 Uniswap,PancakeSwap 等 DEX 的朋友应该有印象,在移除 LP 流动性的时候,我们需要先签名,然后再发送一笔交易移除流动性。正常情况下,其实应该我们先调用 LP 代币的授权方法,授权 DEX 合约可以转移我们的 LP,然后再去移除流动性。而这种二合一的实现正是应用了 EIP-712。它帮助我们仅仅签名一次,就可以将两步交易合并为一步交易,从而节省 Gas 费用。这篇文章我们就来看看 EIP-712 到底是怎么使用的。基本结构EIP712Domain顾名思义,是一个与域相关的结构体,总共包含五个字段:name,合约或者协议的名称version,合约的版本chainId,合约部署的链 Id,一般使用 ...
流动性挖矿-合约原理详解
流动性挖矿应该是上个牛市最火热的内容,基本上整个 DeFi 都是在围绕着流动性挖矿展开的,今天我们就来看看它到底是什么以及合约代码层面是怎么实现的。流动性挖矿简介首先我们先从用户的角度来理解一下流动性挖矿是什么,实际上就是用户通过在合约中质押一个 token 从而赚取另一个 token 的过程。例如,SushiSwap 最初推出的 DEX 流动性挖矿,用户可以通过将 SushiSwap 的 LP token 质押到合约中赚取 Sushi token。那么这个奖励具体是怎么发放以及如何实现的呢?我们今天就来研究一下这部分内容。 先来看几个例子: 一:假设有一个流动性挖矿的合约,可以质押 A token 赚取 B token。它在 0 秒时开始活动,每秒奖励 R 个 B token。此时有用户 Alice 在第 3 秒时质押了 2 个 A token,并且之后没有其他人参与,在第 8 秒时取出 token,图示:那么他在此时获得的收益就是:5R = (2 / 2) * (8 - 3) * R 其中,第一个 2 是用户 A 质押的数量,第二个 2 是合约中质押的总量,(8-3)是用户 ...
CREATE2 操作码使用方法详解
CREATE2 是一个可以在合约中创建合约的操作码。我们先来举个例子看看它能干什么:这段代码是 Uniswap v2-core 里面的工厂合约代码,使用 create2 操作码创建了 pair 合约,返回值是 pair 的地址,这样就可以逻辑中直接使用其地址进行接下来的操作。 那么 create2 到底是怎么使用呢,根据官方 EIP 文档,create2 一共接收四个参数,分别是:endowment(创建合约时往合约中打的 ETH 数量)memory_start(代码在内存中的起始位置,一般固定为 add(bytecode, 0x20) )memory_length(代码长度,一般固定为 mload(bytecode) )salt(随机数盐)这里要注意的是第一个参数如果大于 0 的话,需要待部署合约的构造方法带有 payable。随机数盐是由用户自定,须为 bytes32 格式,例如在上面 Uniswap 的例子中,salt 为:bytes32 salt = keccak256(abi.encodePacked(token0, token1)); create2 还有一个优点,相...
Smart Contract Developer

Subscribe to xyyme.eth

Subscribe to xyyme.eth
Share Dialog
Share Dialog
<100 subscribers
<100 subscribers
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 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 不算是一个额外的功能,它指的是 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 的主要逻辑代码看完,整体来说不算难,业务逻辑相对也比较简单,没有特别绕的逻辑。希望能对大家起一个代码导读的作用,接下来看代码会更加容易。
欢迎和我交流
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 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 不算是一个额外的功能,它指的是 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 的主要逻辑代码看完,整体来说不算难,业务逻辑相对也比较简单,没有特别绕的逻辑。希望能对大家起一个代码导读的作用,接下来看代码会更加容易。
欢迎和我交流
No activity yet