Cover photo

LooksRare平台架构与核心流程剖析

0. 交易场景

0.1 术语

一单完整的交易必须存在2种角色:makertaker

  • maker :账本深度的制造者。

  • taker:账本深度的移除者。在DEX中它扮演者最重要角色,它永远是交易的推动者。

交易中还有2种行为:askbid

  • ask:卖方出价,用NFT换取token。

  • bid:买方出价,用token购买NFT。

以上两两组和之后就会形成一下的成对交易方式:

MakerAsk -- TakerBid:买家出售NFT以换取相应的Token,卖家拿Token换取NFT。

MakerBid --TakerAsk:买家以token申购NFT,卖家接受买家报价并执行交易和兑换。

0.2 释例

交易场景
交易场景

case_1:

User1拥有两个NFT,他对NFT1有卖出意向,于是在交易所中进行了挂单。

User2对NFT1感兴趣,用手里的token购买了NFT1。

User2是交易的实际发起者,他将负担交易的gas fee。

User1--maker,他的行为是ask。

User2--taker,他的行为是bid。

case_2:

User1当前并未打算出售NFT2,所以他并未对其挂单。

User2对NFT2非常着迷,尝试对NFT1进行报价,用token求购之。

NFT2难以拒绝的报价让User1决定接受之。

User1是交易的实际发起者,他将负担交易的gas fee。

User1--taker,他的行为是ask。

User2--maker,他的行为是bid。

1.LooksRare领域模型

1.1 子域划分

子域划分
子域划分

LooksRare总体来说分为三个核心子域,分别是

  • 支付域

    负责合法的交易token管理。

    负责NFT交易的转账管理。

  • 版权域

    负责Collection版权费、平台佣金和用户的收益分成管理。

  • 执行域

    负责执行动作和执行策略的实施。

1.2 平台架构

平台架构
平台架构

Looksrare交易部分当前由四大组件构成:货币管理器、NFT选择器、执行管理器、版权费管理器。

1.2.1 支付域

  • 货币管理器

    货币管理器主要功能是在Looksrare上可以进行交易的合法货币币种进行增加/移除/查询等操作。能执行该操作的人限定是Looksrare平台方。

  • NFT选择器

    NFT选择器主要的职能是选择正确的接口选择器(ERC721,ERC1155)调用safeTransferFrom方法。LooksRare针对非标准的NFT Collection也提供了相应的适配,不过它底层调用的是transferFrom方法存在一定安全风险。

1.2.2 版权域

版权域主要的职责是管理各个Collection的版权费计算器和设置相应的费率条件。

  • 版费设置器

    为Collection的Owner提供设置/修改Collection管理员、版费接收账户和版费,这构成了「版费对象」。

  • 版费注册器

    它的首要职能将collection与设置的自定义版费对象关联起来。其次它还负责运行时计算对应的版费。

    个人觉得这里设计上还有优化空间。还应该增加一个版费计算器接口并实现之。这样注册和计算解耦,也为外部合作者提供了自定义版费计算方式的生态玩法。

  • 版费管理器

    它仅仅充当版权域的「聚合根」作用。

1.2.3 执行域

  • 执行管理器

    askbid的执行动作有不同的执行策略。执行管理器主要负责执行策略的增加/移除查询。官方实现的执行策略如下有:

    • StrategyStandardSaleForFixedPrice

    • StrategyPrivateSale

    • StrategyDutchAuction

    • StrategyAnyItemInASetForFixedPrice

    • StrategyAnyItemFromCollectionForFixedPrice

1.3 实体

1.3.1 Maker订单

struct MakerOrder {
  bool isOrderAsk; // true --> ask【卖】 / false --> bid【买】
  address signer; // signer of the maker order
  address collection; // collection address
  uint256 price; // price (used as )
  uint256 tokenId; // id of the token
  uint256 amount; // amount of tokens to sell/purchase (must be 1 for ERC721, 1+ for ERC1155)
  address strategy; // strategy for trade execution (e.g., DutchAuction, StandardSaleForFixedPrice)
  address currency; // currency (e.g., WETH)
  uint256 nonce; // order nonce (must be unique unless new maker order is meant to override existing one e.g., lower ask price)
  uint256 startTime; // startTime in timestamp
  uint256 endTime; // endTime in timestamp
  uint256 minPercentageToAsk; // slippage protection (9000 --> 90% of the final price must return to ask)
  bytes params; // additional parameters
  uint8 v; // v: parameter (27 or 28)
  bytes32 r; // r: parameter
  bytes32 s; // s: parameter
}

1.3.2 Taker订单

struct TakerOrder {
  bool isOrderAsk; // true --> ask【买】 / false --> bid【卖】
  address taker; // msg.sender
  uint256 price; // final price for the purchase
  uint256 tokenId;
  uint256 minPercentageToAsk; // // slippage protection (9000 --> 90% of the final price must return to ask)
  bytes params; // other params (e.g., tokenId)
}

2.核心流程

2.1 matchAskWithTakerBidUsingETHAndWETH

post image

2.1.1 入参校验

必须满足以下条件:

  • maker是挂单者,售卖NFT;taker是吃单者,用token买NFT。

  • maker接受的货币类型必须是WETH。

  • 调用方是taker。

2.1.2 付款金额校验

  • 付款金额不得大于NFT售价。

  • 如果付款金额小于NFT售价,则尝试用msg.sender账户的WETH转移到该合约WETH账户。

2.1.3 ETH统一转换为WETH存入合约WETH账户

2.1.4 Maker订单信息校验

订单信息校验又可以分为以下步骤:

post image

a.基础校验

  • maker的该订单没有被执行过

  • maker的nonce不低于合约中user的最小执行nonce

  • maker发起者不是address(0)

  • maker的出售数额>0

b.maker签名校验

这里使用https://eips.ethereum.org/EIPS/eip-712 规范进行校验。

  • 如果是调用方是合约,验证是否接口满足InterfaceSelector。

  • 如果调用方是外部地址,则从hash中恢复出signer',判断外部传入signer == singer' 。

c.交易货币合法性校验

d.交易策略合法性校验

2.1.5 执行参数校验

使用maker.strategy的地址所代表出售策略执行

IExecutionStrategy.canExecuteTakerBid(TakerOrder taker, MakerOrder maker),其中可以被maker事先选择执行的合法策略有:

  • StrategyStandardSaleForFixedPrice (固定价格销售)

    {
        return (
            ((makerAsk.price == takerBid.price) &&
                (makerAsk.tokenId == takerBid.tokenId) &&
                (makerAsk.startTime <= block.timestamp) &&
                (makerAsk.endTime >= block.timestamp)),
            makerAsk.tokenId,
            makerAsk.amount
        );
    }
    
  • StrategyPrivateSale(售卖给指定用户)

    {
      // 从maker签名中反解出指定售卖人
      address targetBuyer = abi.decode(makerAsk.params, (address));
    
      return (
        ((targetBuyer == takerBid.taker) && //msg.sender必须为buyer才能进行下去
         (makerAsk.price == takerBid.price) &&
         (makerAsk.tokenId == takerBid.tokenId) &&
         (makerAsk.startTime <= block.timestamp) &&
         (makerAsk.endTime >= block.timestamp)),
        makerAsk.tokenId,
        makerAsk.amount
      );
    }
    
  • StrategyDutchAuction(荷兰式拍卖)

    {
      uint256 startPrice = abi.decode(makerAsk.params, (uint256)); //maker设定的起拍价
      uint256 endPrice = makerAsk.price; //maker设定的底价
    
      uint256 startTime = makerAsk.startTime; //maker设定的拍卖起止时间
      uint256 endTime = makerAsk.endTime;
    
      require(endTime >= (startTime + minimumAuctionLengthInSeconds), "Dutch Auction: Length must be longer");
      require(startPrice > endPrice, "Dutch Auction: Start price must be greater than end price");
    
      //随着时间流逝,当前拍卖价格会逐渐接近于maker的底价
      uint256 currentAuctionPrice = startPrice -
        (((startPrice - endPrice) * (block.timestamp - startTime)) / (endTime - startTime));
    
      return (
        (startTime <= block.timestamp) && //拍卖时间内才运行成交
        (endTime >= block.timestamp) &&
        (takerBid.price >= currentAuctionPrice) && //只要买家出价不小于当前拍卖喊价即可
        (takerBid.tokenId == makerAsk.tokenId),
        makerAsk.tokenId,
        makerAsk.amount
      );
    }
    

2.1.6 费用结算

费用结算主要是对taker的付款金额按照协议费、版权费清算后再将剩余款项转给卖家。

1)协议费结算

协议费结算也与maker选定的执行策略先关,主要是使用

IExecutionStrategy.viewProtocolFee(),其中合法的策略如【2.1.5】阐述所示,其中对应的协议率均为各自contract部署的时候设置,且无修改接口。

协议费的计算公式是

协议费 = 交易金额 * 协议费率

协议费以WETH的形式转入交易所协议费收款账户protocolFeeRecipient,剩下的钱继续后续结算。from账户是合约WETH账户

2)版权费结算

版权费的结算“费基” 也是原始交易金额。

版权费是结算给对应Collection方,并且各个Collection的版权费收款地址和费率各不相同。这些版权费信息被封装在一个个FeeInfo对象中,并且Collection与FeeInfo的映射关系在RoyaltyFeeRegistry_royaltyFeeInfoCollection存放。

struct FeeInfo {
  address setter; //版权费设置人
  address receiver;//版权费收款人
  uint256 fee;//版权费率
}

LooksRare未遵循ERC-2918协议,接口采用royaltyInfo(addresscollection, uint256amount) returns (address, uint256),返回合约版权费收款账户和版权费。

协议费的计算公式是

版权费 = 交易金额 * 版权费率

*如果Collection未在LooksRare设置版权费信息,则会尝试调用Collection实现的ERC2981协议计算版权费。

3)卖家结算

卖家收款金额 = 交易金额 - 协议费 - 版权费

卖家的结算也会以WETH的形式从交易所合约WETH账户转入。

2.1.7 NFT结算

1)选择转让管理器TransferManager

在之前被注册到交易所上的transferSelectorNFT中根据Collection地址寻找合适的转让管理器。Collection与transferManger地址的映射关系存放于transferManagerSelectorForCollection中。

如果Collection没有绑定对应的TransferManager,LooksRare会根据Collection实现的接口进行判断,如果实现了ERC721,则用TRANSFER_MANAGER_ERC721,如果实现ERC1155,则用INTERFACE_ID_ERC1155

2)根据不同的transferManager,实际调用的是标准的IERC721/1155的safeTransferFrom方法。

2.2 matchAskWithTakerBid

与【2.1】重要区别是付款方式不再是ETH或WETH,而是另外一些ERC20。

post image

2.2.1 基本参数校验

必须满足以下条件:

  • maker是挂单者,售卖nft;taker是吃单者,用token买nft。

  • 调用方是taker。

2.2.2 费用结算

费用结算与【2.1.6】的协议费、版权费和卖家结算金额计算方式相同,唯一不同之处是采用maker认定的货币类型进行结算。

IERC20(currency).safeTransferFrom(from, protocolFeeRecipient, protocolFeeAmount);

2.3 matchBidWithTakerAsk

post image

2.3.1 基本参数校验

  • maker是挂单者,用token买nft;taker是吃单者,nft换token。

  • taker是调用者

2.3.2 maker订单信息校验

与之前相同。

2.3.3 执行参数校验

使用maker.strategy的地址所代表申购策略执行。

IExecutionStrategy.canExecuteTakerAsk(TakerOrder taker, MakerOrder maker),其中可以被maker事先选择执行的合法策略有:

  • StrategyStandardSaleForFixedPrice (固定价格申购)

    {
        return (
            ((makerBid.price == takerAsk.price) &&
                (makerBid.endTime >= block.timestamp) &&
                (makerBid.startTime <= block.timestamp)),
            takerAsk.tokenId,
            makerBid.amount
        );
    }
    
  • StrategyAnyItemInASetForFixedPrice(固定价格购买指定集合中的任意Item)

    {
        // Precomputed merkleRoot (that contains the tokenIds that match a common characteristic)
            bytes32 merkleRoot = abi.decode(makerBid.params, (bytes32));
    
        // MerkleProof + indexInTree + tokenId
        bytes32[] memory merkleProof = abi.decode(takerAsk.params, (bytes32[]));
    
        // Compute the node
        bytes32 node = keccak256(abi.encodePacked(takerAsk.tokenId));
    
        // Return whether the order can be executed, the tokenId, and the amount to sell
        return (
            (MerkleProof.verify(merkleProof, merkleRoot, node)&&
                (makerBid.price == takerAsk.price) &&
                (makerBid.endTime >= block.timestamp) &&
                (makerBid.startTime <= block.timestamp)),
                takerAsk.tokenId,//这里体现出任意,即Set持有者选一个给maker
                makerBid.amount
        );
    }
    
  • StrategyAnyItemFromCollectionForFixedPrice(固定架构购买指定Collection中任意item)

    {
        return (
            ((makerBid.price == takerAsk.price) &&
                (makerBid.endTime >= block.timestamp) &&
                (makerBid.startTime <= block.timestamp)),
            takerAsk.tokenId,//这里体现出任意,即Collection持有者选一个给maker
            makerBid.amount
        );
    }
    

2.3.4 NFT结算

与前面不同的是,这里的gas费是由NFT所有人支付。

2.3.5 费用结算

strategy:申购者选定的策略

currency:申购者选定的货币类型

amount:卖家确定的售价

1)协议费结算

与【2.1.6】中的协议费结算类似。

协议费计算公式依然是,

协议费 = 交易金额 * 协议费率

所以,不管交易是由买家还是卖家推动,LooksRare交易所的协议费收费方式都一致。

2)版权费结算

与【2.1.6】中的协议费结算类似。

版权费计算公式依然是,

版权费 = 交易金额 * 版权费率

所以,不管交易是由买家还是卖家推动,Collection版权费收费方式都一致。

3)卖家结算

与【2.1.6】中的结算方式一致。

卖家收款金额 = 交易金额 - 协议费 - 版权费

当然货币类似是taker指定的货币类型。

3. 总结

LooksRare的代码框架还是非常整洁,代码逻辑清晰,分工职责合理。同样这套架构框架,稍微改改即可变成「NFT租赁平台」。

LooksRare最大的吸引点是「挂单奖励」,与租赁市场相结合后会擦出怎样的火花我们拭目以待。

4. 参考文档

https://docs.looksrare.org/developers/looksrare-exchange-overview

https://github.com/LooksRare/contracts-exchange-v1