近期,NFT 市场 OpenSea 宣布推出全新 Web3 市场协议 Seaport 协议,用于安全高效地买卖 NFT。本文将深度分析其关键业务实现和接口实现。
SeaPort官方文档 :https://docs.opensea.io/v2.0/reference/seaport-overview , 可配合查阅,进一步加深理解。
Uniswap 用开源去中心化交易改变了加密货币交易的游戏规则,这是我们现在所知的 2020 年 DeFi Summer的开始,也带来了 DEX 和 DeFi 的大规模增长和创新 。OpenSea的新协议Seaport或许也有改变NFT交易游戏规则的潜力,这也是我们分析Seaport协议的原因。
Seaport 是一个市场合约,用于安全有效地创建和执行 ERC721 和 ERC1155 代币的订单。 每个订单包含任意数量的供应商愿意提供的物品(“报价(offer)”)以及任意数量的必须连同其各自的接收者一起接收的物品(“对价(consideration)”)。
1. 开源代码:
有了 Seaport 协议,任何人都可以使用该协议构建一个 NFT 市场,因为它是去中心化和开源的。在未来几年,我们应该会看到更多的 NFT 市场建立起来。更多的竞争=更好+更快的创新
2. 去中心化:
OpenSea 说这个协议没有合约所有者,任何人都可以更新或生成代码。
3. 交易新范式:
与一些平台只能用加密货币换取 NFT 不同,Seaport 协议允许用户以一系列新方式获取 NFT,投标人(或报价者)可以捆绑不同的资产(如提供 ETH/ERC20/ERC721/ERC1155 资产)以换取 NFT。
4. 交易特定的 NFT:
当交易 NFT 时,你也可以设置 NFT 必须具备的特定“条件”。
5. 荷兰式拍卖列表:
在 Seaport 协议中,你可以设置一个开始和结束价格,表明你希望拍卖持续多长时间。该列表将降低(或提高)价格,直到找到买家(或拍卖时间到)。
6. 更高的安全性:
OpenSea 正在进行为期两周的协议审计竞赛,奖金总额为 100 万美元。任何开发人员都可以审核代码,提交他们发现的评审和错误,并获得奖励。
每一个订单都包含11个关键组件:
offerer
订单的报价者提供了所有的供应代币并且必须亲自执行订单(即msg.sender == offerer
)或者通过签名(标准的65字节 ECDSA,64字节 EIP-2098 或 EIP-1271isValidSignature
检查)或列举链上订单(即调用validate
)来批准订单。
zone
订单的区域是附加到订单的可选辅助帐户,具有两个额外的权限:
该区域可以通过调用cancel
来取消命名为该区域的订单。(注意,报价者仍可以取消他们自己的订单,可以单独取消,也可以通过调用incrementNonce
立刻取消由其当前 nonce 签名的所有订单)。
“受限”订单(有订单类型声明)必须由区域或报价者执行,或者必须通过调用区域上的isValidOrder
或isvalidOrderIncludingExtraData
视图函数来获得批准。
offer
报价包含可以从报价者帐户转移的一系列代币,其中每个代币由以下组件组成:
itemType
指定代币类型,有效类型包括 Ether(或者其他指定链的原生代币)、ERC20、ERC721、ERC1155、ERC721、有“条件(criteria)”的 ERC721 以及有“条件(criteria)”的 ERC1155。
token
指定代币合约的账户地址(空地址用于以太币或其他原生代币)。
identifierOrCriteria
表示 ERC721 或 ERC1155 代币标识符,或者在基于条件的代币类型的情况下,表示由代币的有效代币标识符集合组成的 merkle 根。对于 Ether 和 ERC20 类型 ,该值会被忽略,并且对于基于条件的代币类型,可以将值设置为 0 以允许任何标识符。
startAmount
表示如果在订单激活时完成订单所需要的相关代币的数量。
endAmount
表示如果在订单到期时执行订单所需要的相关代币的数量。如果此值与startAmount
不同,则根据订单激活后经历的时间线性计算出实际的数量。
consideration
包含为完成订单而必须接收的代币数组。它包含所有与所提供代币相同的组件,并且还包括一个用于接收每个代币的recipient
组件。该数组可以由执行者在订单执行时进行扩展,以支持“小费”(例如中继费或推荐费)。
orderType
订单类型,根据两个不同的偏好,指定订单的四种类型之一,:
FULL
表示不支持部分填充,而PARTIAL
允许填充订单中的一部分,注意每个代币必须被提供的分数完全整除(即除法后没有余数)。
OPEN
表示任意账户都可以提交执行订单的调用,而RESTRICTED
则需要订单必须由报价者或订单所在区域执行,或者在区域上调用isValidOrder
或isValidOrderIncludingExtraData
视图函数时返回表示订单被批准的神奇的值。
startTime
表示订单激活时的区块链时间。
endTime
表示订单到期的区块链时间。该值与startTime
与每个代币的startAmount
和endAmount
一起使用以得出它们的当前数量。
zoneHash
表示一个任意的 32 字节值,当执行受限订单时,该值将提供给区域,该区域在确定是否是授权订单时可以使用该值。
salt
表示订单的任意熵源。
conduitKey
是一个bytes32
类型的值,表示在执行转移时应将哪个渠道(conduit)(如果有)用作代币批准的来源。默认情况下(即当conduitKey
设置为零哈希时),报价方将直接向 Seaport 授予 ERC20、ERC721 和 ERC1155 代币批准,以便它可以在执行期间执行订单指定的任何转移。相反,选择使用渠道的报价者将授予与提供的渠道密钥相对应的渠道合约的代币批准,然后 Seaport 指示该渠道转移相应的代币。
nonce
表示必须与给定报价者的当前随机数匹配的值。
订单通过以下4种方式中的一种来执行:
调用两个“标准”函数fulfillOrder
和fulfillAdvancedOrder
中的一个,并且构造第二个隐含订单,同时其调用者作为报价者(offerer),已执行订单的对价(consideration)作为报价(offer),已执行订单的报价作为对价(使用“高级”订单包含应与一组“条件解析器”一起填写的部分,这些“条件解析器”为已执行订单上的每个基于条件的代币指定一个标识符和相应的包含证明)。所有报价代币将从订单报价者转移到执行者,然后所有对价代币将从执行者转移到指定的接收者。
调用"基本"函数fulfillBasicOrder
,并提供六种基本路线类型(ETH_TO_ERC721
、ETH_TO_ERC1155
、ERC20_TO_ERC721
、ERC20_TO_ERC1155
、ERC721_TO_ERC20
以及ERC1155_TO_ERC20
)中的一种,将从组件子集派生要执行的订单,假设相关订单符合以下条件:
该订单仅包含一个报价代币,并且包含至少一个对价(consideration)代币。
该订单仅包含一个 ERC721 或 ERC1155 代币,并且该代币不是基于条件的。
订单的报价者是第一个对价代币的接收者。
所有其他代币都具有相同的以太币(或其他原生代币)或 ERC20 项目类型和代币。
该订单不提供以以太币(或其他原生代币)作为其项目类型的项目。
每个项目上的startAmount
必须与该项目的endAmount
匹配(即项目不能有升序/降序数量)。
所有“忽略”的项目字段(即token
和原生代币项目中的identifierOrCriteria
以及 ERC20 项目中的identifierOrCriteria
)都设置为空地址或零。
如果订单中有 ERC721 项目,则该项目的数量为.1
如果订单有多个对价(consideration)项目,且除了第一个对价项目以外的所有对价项目与报价项目的项目类型相同,报价项目数量不小于除了第一个对价项目数量外的所有对价项目数量之和。
调用两个“可用执行”函数(fulfillAvailableOrders
和fulfillAvailableAdvancedOrders
)中的一个,并且提供一组订单与一组执行声明,其中的执行声明指定哪些报价项目可以聚合到不同的转移中,相应地哪些对价项目可以聚合在一起,以及其中已经取消的订单是因为时间无效,或者已经完全成交的订单将被跳过,而不会导致其余可用订单回滚。此外,一旦锁定maximumFulfilled
可用订单,剩余的所有订单都将被跳过。与标准执行函数类似,所有报价项目将从各自的报价者转移到执行者,然后所有对价项目将从执行者转移到指定的接收者。
调用两个“匹配”函数(matchOrders
和matchAdvancedOrders
)中的一个,并且提供一组明确的订单以及一组执行,该执行指定了哪些报价项目应用于哪些对价项目(并且“高级”案例以类似的方式运行标准方法,但支持通过提供的分子numerator
和分母denominator
小数值以及可选的extraData
参数进行部分填充,当执行受限订单类型时,这些参数将作为调用区域上的isValidOrderIncludingExtraData
视图函数的一部分提供)。 请注意,以这种方式执行的订单没有明确的执行者; 相反,Seaport 将简单地确保每个订单的需求一致。
虽然标准方法在技术上可用于执行任何订单,但在某些情况下存在关键的效率限制:
与简单的“热路径(hot path)”的基本方法相比,它需要额外的调用数据。
它要求执行者批准每个对价项目,即使对价项目可以使用报价项目来执行(在执行为 ERC721 或 ERC1155 项目提供 ERC20 项目并且还包括具有相同的考虑对价的订单时通常是这种情况用于支付费用的 ERC20 项目类型)。
它可能导致不必要的转移,而在“匹配”情况下,这些转移可以减少到更小的集合。
创建报价时,应检查以下要求以确保订单可以执行:
报价者应在所有报价项目中有足够的余额。
如果订单未指明使用渠道,则报价者应为所有提供的 ERC20、ERC721 和 ERC1155 项目的 Seaport 合约设置足够的批准。
如果订单确实指明了使用渠道,则报价者应为所有提供的 ERC20、ERC721 和 ERC1155 项目的相应渠道合约设置足够的批准。
执行基本订单时,需要检查以下要求以确保订单可以执行:
需要执行上述检查以确保报价者仍有足够的余额和批准。
执行者应该对所有对价项目有足够的余额,除了那些项目类型与订单提供的项目类型相匹配的项目——例如,如果执行的订单提供 ERC20 项目,并且要求向报价者提供 ERC721 项目并且向另一个接受者提供相同的 ERC20 项目,那么执行者需要拥有 ERC721 项目,但不需要拥有 ERC20 项目,因为它将来自报价者。
如果执行者不选择使用渠道,他们需要为已执行订单上所有的 ERC20、ERC721 和 ERC1155 对价项目设置足够的 Seaport 合约批准,项目类型与订单提供的项目类型匹配的 ERC20 项目除外。
如果执行者确实选择使用渠道,则他们需要为已执行订单上的所有 ERC20、ERC721 和 ERC1155 对价项目为其各自的渠道设置足够的批准,项目类型与订单提供的项目类型匹配的 ERC20 项目除外.
如果已执行的订单将以太币(或其他原生代币)指定为对价项目,则执行者必须能够将这些项目的总金额提供为msg.value
执行标准订单时,需要检查以下要求以确保订单可以执行:
需要执行上述检查以确保报价者有足够的余额和批准。
在收到所有的报价项目后,执行者应该对所有的报价项目有足够的余额——例如,如果执行的订单提供了 ERC20 项目,并且需要向报价者提供 ERC721 项目,并且向另一个接收者提供相同的 ERC20 项目,其数量小于或等于提供的数量,执行者不需要拥有 ERC20 项目,因为它将最先从报价者处接收到。
如果执行者不选择使用渠道,他们需要为已执行订单上的所有 ERC20、ERC721 和 ERC1155 对价项目的 Seaport 合约设置足够的批准。
如果执行者确实选择使用渠道,则他们需要为已执行订单上的所有 ERC20、ERC721 和 ERC1155 对价项目其各自的渠道设置足够的批准。
如果已执行的订单将以太币(或其他原生代币)指定为对价项目,则执行者必须能够将这些项目的总数量提供为msg.value
在执行一组匹配订单时,需要检查以下要求以确保订单可以执行:
作为执行的一部分执行,执行采购 ERC20、ERC721 或 ERC1155 项目的每个帐户必须在触发执行时在 Seaport 或指定的渠道上具有足够的余额和批准。请注意,先前的执行可能会为后续执行提供必要的余衡。
涉及以太币(或其他原生代币)的所有执行的总和必须以msg.value
的形式提供. 请注意,提供者和接收者是同一帐户的执行将从最终执行集中被过滤掉。
在构建订单时,报价者可以选择通过设置适当的订单类型来启用部分成交。然后,支持部分执行的订单可以在相应订单的某一部分中执行,从而允许后续执行绕过签名验证。总结一下部分填充的几个关键点:
当创建支持部分成交的订单或确定这些订单要成交的部分时,订单上的所有项目(报价和对价)数量必须能被提供的部分项目数量完全整除(即除法后没有余数)。
如果要填写的所需部分会导致要填写的订单数量超过全部订单金额,则该部分将减少为剩余要填写的数量。这适用于部分填充尝试和完全填充尝试。如果不需要这种行为(即填充应该是“全部或无”),则执行者可以使用“基本”订单方法(如果可用)(这需要填写全部订单数量),或使用“匹配” 订单方法,并明确提供一个要求收到全部所需金额的订单。
举例来说:如果一个执行者尝试执行订单的 1/2,但另一个执行者首先执行订单的 3/4,则原始执行者最终将执行订单的 1/4。
如果部分可成交订单上的任一项目指定了不同的startAmount
和endAmount
(例如,它们是递增数量或递减数量的项目),则在确定当前价格之前,该分数将应用于这两个数量。这确保了在构建订单时可以选择完全可分的金额,而不依赖于最终完成订单的时间。
部分成交可以与基于条件的项目进行组合,以支持构建提供或接收多个项目的订单,否则这些项目将无法部分成交(例如 ERC721 项目)。
举个例子:报价者可以创建一个部分可成交的订单,为给定集合中最多 10 个 ERC721 项目提供最多 10 个 ETH;然后,任何执行者都可以执行该订单的一部分,直到它被完全执行(或取消)。
当通过fulfillOrder
或fulfillAdvancedOrder
来执行订单时:
计算订单哈希值
计算报价项目和对价项目的哈希值
检索报价者的当前计数器
计算订单哈希值
执行初始化校验
确保当前时间在订单有效时间内
确保调用者对于当前订单类型是有效的; 如果订单类型收到限制且调用者不是offerer
或者zone
,调用zone
判断订单是否有效
检索并更新订单状态
确保订单未被取消
确保订单没有被全部执行
如果订单是部分执行的,如有必要,减少提供的执行数量,以免订单被过度执行
若订单签名尚未验证,则验证订单签名
根据偏好 + 可用金额 (preference + available amount) 确定要执行的分数
更新订单状态(已验证+执行分数)
确定每个项目的金额
比较初始金额startAmount
和结束金额endAmount
若相等,将执行分数应用于该金额,确保结果是整数,然后使用该结果
若不等,对这两个金额都应用执行分数,确保两个结果都是整数,然后根据当前时间找到这两个结果的现行拟合值
应用条件解析器
确保每一个条件解析器都应用于一个基于条件的订单项目
如果项目具有一个非零的条件根值(a non-zero criteria root),确保为每个项目提供的标识符是有效的
更新每个项目的类型和标识符
确保所有剩余的项目都不是基于条件的项目
触发OrderFulfilled
事件
包括更新的项目(即在金额调整和条件解决之后)
将报价项目(代币)由报价者转移到调用者
使用渠道或 Seaport 直接获得批准,具体取决于订单的类型
将对价项目(代币)有调用者转移到对应的接受者
使用渠道或 Seaport 直接获得批准,具体取决于执行者声明的偏好
当通过matchOrders
或者matchAdvancedOrders
来匹配一组订单时,步骤 1 到 6 几乎完全相同,但针对每个提供的订单执行。从这里开始,执行与上面的标准执行不同:
应用执行
确保每次执行都涉及一个或多个报价项目和一个或多个对价项目,所有这些项目都具有相同的类型和代币,并且每个报价项目具有相同的批准源以及每个对价项目具有相同接受者
将每个报价项目和对价项目的金额减少到零,并跟踪其总减少金额
比较每个项目的总金额,并将剩余金额加回相应订单一侧(报价项目或对价项目)的第一个项目
为每个成交返回一个执行
扫描每个对价项目并确保没有一个对价项目仍然有非零的剩余金额
作为每次执行的一部分进行转账
根据原始订单类型,直接使用渠道或 Seaport 获得批准
忽略to == from
或amount == 0
时的每次执行(注意:当前实现不执行最后一次优化)
Seaport 是一个通用的 ETH/ERC20/ERC721/ERC1155 市场。它最大限度地减少了外部调用,并为普通路由提供了轻量级的方法,以及更灵活的方法来组合高级订单。
ConsiderationInterface 包含 Seaport 的所有外部函数接口。
执行基本订单,仅支持Ether(或指定链的原生代币)与 ERC721 之间的交易。
提取订单类型和基本订单路由,并且对其进行校验
准备执行基本订单
(1)添加重入锁
(2)校验时间正确
(3)检验参数正确
(4)计算并校验订单的哈希值
(5)更新订单状态
若使用了渠道,则根据订单路由导出渠道
根据订单路由,执行原生代币以及ERC721代币的转账,完成订单
删除重入锁
fulfillOrder 执行普通订单,不支持订单部分执行,不支持条件解析器;普通订单作为特殊的高级订单进行执行。
fulfillAdvancedOrder 执行高级订单。
添加重入锁
_validateOrderAndUpdateStatus
:根据参数,验证订单,更新状态并计算订单哈希值orderHash
、需要执行订单的分子numerator
和分母denominator
(1)时间校验:startTime <= block.time <= endTime
(2)分子与分母校验:
numerator <= denominator && denominator != 0
;
若numerator == denominator
,需要支持部分执行(Support Partial Fills)
(3)对价项目长度校验以及计算订单哈希值orderHash
:
订单长度校验:orderParameters.consideration.length >= orderParameters.totalOriginalConsiderationItems
,即参数中consideration
数组实际长度要大于或等于参数中直接传递的原始对价项目总数
计算订单哈希值orderHash
(4)校验高级订单的有效性:订单类型为 2 或 3 要求 zone 或 offerer 是 caller 或 者 zone 批准。
(5)校验订单状态orderStauts = _orderStatus[orderHash]
,保证订单没有被取消并且是可执行的
订单未取消,即:保证orderStauts.isCancelled == false
订单可执行,即:若orderStatus.numerator != 0
,保证orderStatus.numerator < orderStatus.denominator
。
(6)校验订单签名,即若orderStatus.isValidated == false
,则调用_verifySignature
函数
(7)计算需要执行订单的分子fillNumerator
和分母fillDenominator
(8)更新状态变量orderStatus
_applyCriteriaResolvers
:用条件解析器,对每个生成的订单类型以及条件解析器的绑定进行校验,确保提交的待执行订单是有效的
_applyFractionsAndTransferEach
:以指定的分数值执行每个项目的转账
_emitOrderFulfilledEvent
:发出一个表明订单已完成的事件。
删除重入锁
fulfillAvailableOrders 执行可用普通订单,不支持订单部分执行,不支持条件解析器;可用普通订单作为特殊的可用高级订单进行执行
fulfillAvailableAdvancedOrders 执行可用的高级订单
_validateOrdersAndPrepareToFulfill
:校验订单(若有无效订单则跳过)、更新其状态、通过先前填充的分数减少金额、应用条件解析器并触发 OrderFulfilled 事件。
(1)添加重入锁
(2)声明并设置一个错误缓冲区变量,指明任何本地报价项目的状态。
(3)循环遍历每一个订单,校验订单,更新每一个订单中的参数:
_validateOrderAndUpdateStatus
:根据参数,验证订单,更新状态并计算订单哈希值orderHash
、需要执行订单的分子numerator
和分母denominator
循环遍历订单中的每一个报价项目,更新报价项目中的startAmount
和endAmount
循环遍历订单中的每一个对价项目,更新对价项目中的startAmount
和endAmount
(4)校验错误缓冲区变量
(5)_applyCriteriaResolvers
:应用条件解析器,对每个生成的订单类型以及条件解析器的绑定进行校验,确保提交的待执行订单是有效的聚合已使用的报价和对价项目并执行转账
(6)触发 OrderFulfilled 事件
_executeAvailableFulfillments
:完全或部分执行通过了校验的订单,每个订单具有任意数量的要约和考虑项目,并执行转移。 任何当前未激活、已完全成交或已取消的订单都将被忽略。 然后,剩余的报价和考虑项目将在可能的情况下聚合,如所提供的报价和考虑组件数组所示,并且聚合的项目将分别转移到履行者或每个预期的接收者。 请注意,失败的项目转移或订单格式问题将导致整个批次失败。
(1)为每一个订单的报价和对价项目的执行分配一个Execution
结构,构造为一个Execution
结构数组executions
,任何当前未激活、已完全成交或已取消的无效订单都将被忽略。
(2)校验executions
的长度
(3)对executions
中的每一个Execution
结构对象进行校验,过滤掉当前未激活、已完全成交或已取消的所有订单。
(4)_performFinalChecksAndExecuteOrders
:对高级订单以及executions
进行最后的校验,然后执行订单,完成执行转账,删除重入锁。
matchOrders
:对普通订单中的报价和对价项目按参数fulfillments
提供的报价组件分配给对价组件的条件求进行匹配然后执行,不支持订单部分执行,不支持条件解析器;普通订单作为特殊的高级订单进行处理
matchAdvancedOrders
:对高级订单中的报价和对价项目按参数fulfillments
提供的报价组件分配给对价组件的要求进行匹配然后执行
_validateOrdersAndPrepareToFulfill
:校验订单(若有无效订单则回滚)、更新其状态、通过先前填充的分数减少金额、应用条件解析器并触发 OrderFulfilled 事件。若有无效订单,则回滚。
_fulfillAdvancedOrders
:在验证、调整金额和应用条件解析器之后,执行高级订单
(1)为每一个订单的报价和对价项目的执行分配一个Execution
结构,构造为一个Execution
结构数组executions
(2)循环遍历参数fulfillments
,将executions
中的每一个元素对应到fulfillments
的每一个元素,若报价人者(offerer
)和接受者(recipient
)是相同的,则跳过。
(3)_performFinalChecksAndExecuteOrders
:对高级订单以及executions
进行最后的校验,然后执行订单,完成执行转账,删除重入锁。