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

By [Webber Woo](https://paragraph.com/@webber-woo) · 2022-07-31

---

0\. 交易场景
--------

0.1 术语
------

一单完整的交易必须存在2种角色：`maker`和`taker`。

*   maker ：账本深度的制造者。
    
*   taker：账本深度的移除者。在DEX中它扮演者最重要角色，它永远是交易的推动者。
    

交易中还有2种行为：`ask`和`bid`

*   ask：卖方出价，用NFT换取token。
    
*   bid：买方出价，用token购买NFT。
    

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

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

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

0.2 释例
------

![交易场景](https://storage.googleapis.com/papyrus_images/0608fe507076c3d65a2ffa113e55e4ea91578e44c0e51ccafd871bc595c998f3.png)

交易场景

### 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 子域划分
--------

![子域划分](https://storage.googleapis.com/papyrus_images/282e32f59c8d37fa33ea69854e6ff68476538d6d7542199fa04bf76ee45614d6.png)

子域划分

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

*   支付域
    
    负责合法的交易token管理。
    
    负责NFT交易的转账管理。
    
*   版权域
    
    负责Collection版权费、平台佣金和用户的收益分成管理。
    
*   执行域
    
    负责执行动作和执行策略的实施。
    

1.2 平台架构
--------

![平台架构](https://storage.googleapis.com/papyrus_images/078fceb5050626c002a10d982aae945cfcfb2d173d653f27d9c7bc37e01231d5.png)

平台架构

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 执行域

*   执行管理器
    
    `ask`和`bid`的执行动作有不同的执行策略。执行管理器主要负责执行策略的增加/移除查询。官方实现的执行策略如下有：
    
    *   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
---------------------------------------

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

### 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订单信息校验

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

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

**a.基础校验**

*   maker的该订单没有被执行过
    
*   maker的nonce不低于合约中user的最小执行nonce
    
*   maker发起者不是address(0)
    
*   maker的出售数额>0
    

**b.maker签名校验**

这里使用[https://eips.ethereum.org/EIPS/eip-712](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](https://eips.ethereum.org/EIPS/eip-2981)协议，接口采用`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。

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

### 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
------------------------

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

### 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://docs.looksrare.org/developers/looksrare-exchange-overview)

[https://github.com/LooksRare/contracts-exchange-v1](https://github.com/LooksRare/contracts-exchange-v1)

---

*Originally published on [Webber Woo](https://paragraph.com/@webber-woo/looksrare)*
