如何与 Seaport 交互
- 原文链接:github.com/ProjectOpenS...
- 译者:AI翻译官,校对:翻译小组
- 本文链接:learnblockchain.cn/article…
有关创建订单、履行和与 Seaport 交互的文档。
每个订单包含十一项关键组件:
offerer
提供所有报价的项目,必须亲自履行订单(即 msg.sender == offerer
)或通过签名批准订单(包括标准的 65 字节 EDCSA、64 字节 EIP-2098 或 EIP-1271 的 isValidSignature
检查)或通过在链上列出订单(即调用 validate
)。zone
是附加在订单上的可选次要帐户,具有两个附加权限:
cancel
取消其中命名为区域的订单。(注意,报价人也可以取消自己的订单,无论是单独还是通过调用 incrementCounter
一次性取消所有使用其当前计数器签名的订单)。validateOrder
时由调用者批准,如果调用者不是区域。offer
包含一组可能从报价人的帐户转移的项目, 每个项目由以下组件组成:
itemType
指定项目的类型,合法类型为:NATIVE = 0
ERC20 = 1
ERC721 = 2
ERC1155 = 3
ERC721_WITH_CRITERIA = 4
ERC1155_WITH_CRITERIA = 5
token
指定项目的代币合约的帐户(对于以太或其他原生代币,使用空地址)。identifierOrCriteria
表示 ERC721 或 ERC1155 代币标识符,或在基于标准的项目类型的情况下,由有效代币标识符集合组成的默克尔根。该值将在以太和 ERC20 项目类型中被忽略,对于基于标准的项目类型可以选择设置为零以允许任何标识符。startAmount
表示在订单变为有效时,将在履行订单时所需的相关项目数量。endAmount
表示在订单到期时,将在履行订单时所需的相关项目数量。如果该值与项目的 startAmount
不同,则根据订单变为有效后的经过时间线性计算实际数量。consideration
包含一组必须在履行订单时接收的项目。它包含与报价项目相同的所有组件,并另外包括将接收每个项目的 recipient
。在履行订单时,执行者可以扩展该数组,以支持“附赠”(例如,转发或推荐支付)。orderType
根据三个不同偏好类型指定订单的四种类型之一:
FULL
表示该订单不支持部分填充,而 PARTIAL
允许填充订单的某部分,重要的警告是每个项目必须可以被提供的分数整除(即除法后没有余数)。OPEN
表示可以由任何帐户提交执行订单的调用,而 RESTRICTED
要求订单必须由报价人或区域执行,或者在调用 validateOrder
时返回的魔法值表示订单已获得批准,而调用者不是区域。CONTRACT
表示订单将在报价人发出调用 generateOrder
时生成,然后在执行后通过后续调用 ratifyOrder
在报价人处验证。startTime
指示订单变为有效的区块时间戳。endTime
指示订单到期的区块时间戳。此值和 startTime
与每个项目的 startAmount
和 endAmount
结合使用,以推导出它们的当前数量。zoneHash
表示在履行受限订单时将提供给区域的任意 32 字节值,区域可以利用该值来决定是否授权该订单。salt
表示订单的任意熵源。conduitKey
是一个 bytes32
值,指示在执行转移时应使用哪个导管(如果有的话)作为代币批准的来源。默认情况下(即当 conduitKey
设置为零哈希时),报价人将直接授予 ERC20、ERC721 和 ERC1155 代币批准给 Seaport,以便在履行订单时执行订单所规定的任何转移。相比之下,选择使用导管的报价人将向与所提供的导管密钥对应的导管合约授予代币批准,然后 Seaport 将指示该导管转移相应的代币。counter
表示一个必须与给定报价人的当前计数器匹配的值。订单通过四种方法之一进行履行:
fulfillOrder
和 fulfillAdvancedOrder
,第二个隐含订单将构建,以调用者作为报价人,已履行订单的对价作为报价,已履行订单的报价作为对价(“高级”订单包含应被填充的分数,以及一组“标准解析器”,这些解析器为每个标准项目指定一个标识符和相应的包含证明)。所有提供的项目将从订单的报价人转移到履行者,然后所有对价项目将从履行者转移到指定的收件人。fulfillBasicOrder
,提供六种基本路线类型之一(ETH_TO_ERC721
、ETH_TO_ERC1155
、ERC20_TO_ERC721
、ERC20_TO_ERC1155
、ERC721_TO_ERC20
和 ERC1155_TO_ERC20
),将从一组组件中推导出要履行的订单,假设该订单符合以下条件:
startAmount
必须与该项目的 endAmount
匹配(即项目不能具有递增/递减数额)。token
和 identifierOrCriteria
以及 ERC20 项目的 identifierOrCriteria
)都设置为空地址或零。1
。fulfillAvailableOrders
和 fulfillAvailableAdvancedOrders
,提供一组订单以及一组履行,指定哪些报价项目可以聚合到不同的转移中,哪些对价项目可以相应地聚合,并且任何已被取消、无效或已经完全填充的订单将被跳过,而不会导致其他可用订单回滚。此外,一旦找到 maximumFulfilled
可用订单,任何剩余订单将被跳过。与标准履行方法类似,所有报价项目将从各自的报价人转移到履行者,然后所有对价项目将从履行者转移到指定的收件人。matchOrders
和 matchAdvancedOrders
,提供一组显式订单以及一组履行,指定哪些报价项目应用于哪些对价项目(“高级”案例以类似于标准方法的方式操作,但支持通过提供的 numerator
和 denominator
分数值进行部分填充,以及一个可选的 extraData
参数,该参数将作为调用 validateOrder
函数时的内容提供,或者在履行受限订单类型或调用 generateOrder
和 ratifyOrder
时)。注意,通过这种方式履行的订单没有明确的履行者;相反,Seaport 将确保每个订单之间意愿的巧合。还要注意,合约订单不强制使用特定的导管,但 Seaport 应用可以通过对特定导管的代币设置权限或批准来要求使用特定的导管。如果履行者未提供正确的导管密钥,则调用将回退。目前没有找到给定 Seaport 应用首选的导管的端点。虽然标准方法从技术上讲可以用于履行任何订单,但它在某些情况下存在关键的效率限制:
注意:与 Seaport 的调用,用于履行或匹配一组高级订单的调用可以监控,且如果存在未使用的报价项目,可能会被第三方声称。任何人都可以监控内存池以查找对
matchOrders
或matchAdvancedOrders
的调用,而不涉及“临时”订单(报价人是调用者,因此不需要签名)并计算是否存在未使用的报价项目数量。如果存在未使用的报价项目数量,第三方可以抢先执行交易,并将自己作为收件人,从而使该第三方能够声称未使用的报价项目。Seaport 应用或区域可以阻止这种情况,或者履行者可以利用私有内存池,但默认情况下这是可能的。注意:合约订单可以在执行时提供额外的报价数量。然而,如果它们提供带有标准的额外报价项目,履行者将无法提供必要的标准解析器,这将使执行订单变得不可行。Seaport 应用应特别避免返回基于标准的项目,并一般避免预览订单与链上执行之间的不匹配。
在创建报价时,应检查以下要求以确保订单可履行:
在履行 基础 订单时,需要检查以下要求以确保订单可履行:
msg.value
提供这些项目的总和。在履行 标准 订单时,需要检查以下要求以确保订单可履行:
msg.value
提供这些项目的总和。在履行一组 匹配 订单时,需要检查以下要求以确保订单可履行:
msg.value
提供。注意,报价人和接收人相同帐户的执行将会从最终执行集中过滤掉。在构建订单时,报价人可以通过设置适当的订单类型来选择启用部分填充。然后,支持部分填充的订单可以为相应的订单进行一些 分数 的履行,从而允许后续填充跳过签名验证。总结关于部分填充的一些关键点:
startAmount
和 endAmount
(例如,它们是递增金额或递减金额的项目),则在确定当前价格之前,将对 两个 数量应用分数。这确保在构建订单时可以选择整齐可分的数量,而无需依赖最终履行订单时的时间。部分填充可以与基于标准的项目相结合,允许构建报价或接收多项本来不能部分填充的项目的订单(例如 ERC721 项目)。
在通过 fulfillOrder
或 fulfillAdvancedOrder
履行订单时:
注意:
fulfillBasicOrder
的工作方式类似,但有一些例外:它从一组订单元素重建订单,跳过线性拟合金额调整和标准解析,要求填充完整的订单金额,并在报价项目共享与其他对价项目相同的类型和代币时默认执行更少的转移。
在通过 matchOrders
或 matchAdvancedOrders
匹配一组订单时,第 1 到第 6 步几乎是相同的,但针对 每个 提供的订单执行。从那里开始,实施从标准履行分叉:
to == from
的执行Seaport v1.2 引入了一种新的订单类型:合约订单。简而言之,实现 ContractOffererInterface
的智能合约(在文档中称为“Seaport 应用合约”或“Seaport 应用”)现在可以响应买方或卖方的合约订单请求提供动态生成的订单(合约订单)。支持合约订单使链上流动性与 Seaport 生态系统中的链下流动性处于同一水平。此外,这两种流动性现在具有广泛的可组合性。
这解锁了 Seaport 原生功能的广泛应用,包括从订单中指定的货币(例如 WETH)到履行者首选货币(例如 ETH 或 DAI)的即时转换、闪电贷增强功能、清算引擎等。通常,Seaport 应用允许 Seaport 社区扩展默认的 Seaport 功能。开发者如果有可能作为 Seaport 应用实现的想法或用例,应该在 Seaport 改进协议(SIP)仓库 中发起 PR。
任何人都可以构建与 Seaport 交互的 Seaport 应用合约。Seaport 应用只需遵循以下接口:
interface ContractOffererInterface {
function generateOrder(
address fulfiller,
SpentItem[] calldata minimumReceived,
SpentItem[] calldata maximumSpent,
bytes calldata context
)
external
returns (SpentItem[] memory offer, ReceivedItem[] memory consideration);
function ratifyOrder(
SpentItem[] calldata offer,
ReceivedItem[] calldata consideration,
bytes calldata context,
bytes32[] calldata orderHashes,
uint256 contractNonce
) external returns (bytes4 ratifyOrderMagicValue);
function previewOrder(
address caller,
address fulfiller,
SpentItem[] calldata minimumReceived,
SpentItem[] calldata maximumSpent,
bytes calldata context
)
external
view
returns (SpentItem[] memory offer, ReceivedItem[] memory consideration);
function getSeaportMetadata()
external
view
returns (
string memory name,
Schema[] memory schemas
);
}
请参阅 ./contracts/test/
中的 TestContractOfferer.sol 文件,以获取 MVP Seaport 应用合约的示例。
当 Seaport 从履行者接收到合约订单请求时,它将调用 Seaport 应用合约的 generateOrder
函数,该函数返回一组 SpentItem
和一组 ReceivedItem
。Seaport 应用可以根据其自身规则调整响应,如果其响应符合原始请求者的报价(minimumReceived
)和对价(maximumSpent
)参数规定的可接受范围,Seaport 将执行订单。如果不符合,该调用将回退。
请注意,在请求合约订单时,请求者并未提供常规的、已签名的订单。相反,请求者提供指定 Seaport 应用可操作的可接受范围的参数。
minimumReceived
数组表示请求者愿意从 Seaport 应用合约中接受的最小集合,尽管 Seaport 应用可以提供更多。maximumSpent
数组表示请求者愿意向 Seaport 应用合约提供的最大集合,尽管 Seaport 应用可以接受更少。在非常简单的情况下,请求者的 minimumReceived
数组将成为 Seaport 应用合约订单上的 offer
数组,请求者的 maximumSpent
数组将成为 Seaport 应用合约订单上的 consideration
数组。这两个保护措施可以提供对滑点的保护,以及其他安全功能。
当 Seaport 应用提供额外的报价项目、增加报价项目数量(即自愿超过请求者指定的 minimumReceived
)、删除对价项目或减少对价项目数量(即自愿要求低于请求者指定的 maximumSpent
)时,这些更改统称为“折扣”。当 Seaport 应用尝试提供更少的报价项目、减少报价项目数量、增加对价项目或增加对价项目数量时,这些更改统称为“惩罚”,Seaport 将捕捉并拒绝该订单。
最佳的 Seaport 应用应在其 previewOrder
函数被调用时返回一个带有惩罚的订单,当其 minimumReceived
和 maximumSpent
数组不可接受时,这样调用者可以了解 Seaport 应用的期望。然而,当其 generateOrder
被调用时,如果提供的 minimumReceived
和 maximumSpent
数组不可接受,则应该回退,以便该函数能够快速失败,跳过该调用,并避免浪费Gas,将验证留给 Seaport。
提供给 Seaport 应用合约的第三个参数是 context
,其功能类似于区域的 extraData
参数。例如,提供 AMM 类似功能的 Seaport 应用可能会利用上下文来确定买家偏好的哪些代币 ID,或者在推导订单时是否采取“精确输入”或“精确输出”方法。context
是任意字节,但应该根据提供的标准进行编码,详见 Seaport 改进协议(SIP)仓库。
虽然 SIP 生态系统仍处于早期阶段,但每个订单生成器合约最终都能够找到提供与其用例匹配的 context
编码和解码标准的 SIP。采用一种或多种 SIP 标准化编码或解码方式的订单生成器应根据 SIP 5 中找到的规范作为信号,类似于 EIP 165。
上下文可以留空,或者它可能包含履行合约订单所需的所有信息(代替详细的 minimumReceived
和 maximumSpent
参数)。后一种情况仅应在能够确保相关 Seaport 应用合约可靠时使用,因为使用 minimumReceived
和 maximumSpent
数组将导致 Seaport 执行额外的验证,以确保返回的订单符合履行者的期望。请注意,minimumReceived
是可选的,但 maximumSpent
不是。即使上下文执行大部分工作,maximumSpent
仍必须作为安全保障存在。
合约订单并不像其他 Seaport 订单类型那样提前进行签名和验证,而是由 Seaport 应用合约按需生成。由订单生成器创建的订单的哈希是在 _getGeneratedOrder
中动态获取的,基于 Seaport 应用的地址和每个生成的合约订单递增的 contractNonce
。通过响应 Seaport 的调用,Seaport 应用有效地表明其提供的报价在其角度是可以接受和有效的。
合约订单生命周期包含一个状态 ful 的 generateOrder
调用,用于在执行前派生合约订单,以及在执行后执行的状态 ful ratifyOrder
调用。这意味着合约订单可以在执行前响应例如可替代代币的价格,并在执行后验证闪电贷是否已偿还或 NFT 的某个关键特性在执行过程中未被更改。
请注意,当作为合约订单的输入提供一个全集合标准的项目(标准 = 0)时,Seaport 应用合约完全有自由选择在执行过程中想要使用的任何标识符。这与 Seaport 在其他地方的行为有偏差,在其他地方,履行者可以通过提供标准解析器来选择要接收的标识符。对于标识符或标准 = 0 的合约订单请求,Seaport 不期望提供相应的标准解析器,如果提供则会回退。更多详细信息请参见 _getGeneratedOrder
和 _compareItems
。
在履行时,合约订单可以指定原生代币(例如以太)作为报价项目;订单生成器合约可以直接通过 generateOrder
调用(或以其他方式)将原生代币发送到 Seaport,从而允许履行者使用这些原生代币。任何未使用的原生代币将发送给履行者(即调用者)。只有在重入锁定设置时,原生代币才能发送到 Seaport,并且仅在特定情况下允许。这使得在飞行过程中进行 ETH 与 WETH 之间的转换以及其他可能性成为可能。注意,发送到 Seaport 的任何原生代币将立即可被当前(或下一个)调用者支出。注意,这与 Seaport 在其他地方的行为有所偏差,在其他地方,买方可能无法提供原生代币作为报价项目。
Seaport 也对订单生成合约的正常重入策略做出了例外。订单生成合约可以调用接收钩子并提供原生代币。所有可供 Seaport 应用使用的东西都可以消费,包括 msg.value
和余额。
与订单生成合约交互的买家应注意,在某些情况下,订单生成合约能够通过转移附加在 NFT 上的有价值代币来降低所提供的 NFT 的价值。例如,当 Seaport 调用其 generateOrder
函数时,Seaport 应用可以修改其拥有的 NFT 的属性。在这种情况下,可以考虑使用允许转移后验证的镜像订单,例如合约订单或限制订单。
为了回顾上述讨论的内容,以下是示例合约订单生命周期的描述:
fulfillOrder
并传入一个 Order
结构,其中的 OrderParameters
的 OrderType
为 CONTRACT
。基本上,该订单说:“去 Seaport 应用合约 0x123,告诉它我想购买至少一个 Blitmap。告诉 Seaport 应用我愿意花费最多 10 ETH,但不超过这个金额。”fulfillOrder
调用 _validateAndFulfillAdvancedOrder
,与其他订单类型一样。_validateAndFulfillAdvancedOrder
调用 _validateOrderAndUpdateStatus
,与其他订单类型一样。_validateOrderAndUpdateStatus
内部,当代码路径到达 if (orderParameters.orderType == OrderType.CONTRACT) { ...
的那一行时,合约订单的代码路径与其他订单类型的代码路径分流。_validateOrderAndUpdateStatus
调用 _getGeneratedOrder
。_getGeneratedOrder
进行低级调用到目标订单生成器的 generateOrder
函数。SpentItem
数组和一个 ReceivedItem
数组。在这个示例叙述中,Seaport 应用的响应表示:“好的,我愿意以 10 ETH 售出该 Blitmaps 项目。”_getGeneratedOrder
将外部 generateOrder
调用的结果转换为 Seaport 格式,进行一些检查,然后将订单哈希返回给 _validateOrderAndUpdateStatus
。_validateOrderAndUpdateStatus
通过 _applyFractionsAndTransferEach
转移 NFT 和付款,并进行进一步检查,包括调用 _assertRestrictedAdvancedOrderValidity
。
_assertRestrictedAdvancedOrderValidity
调用 Seaport 应用合约的 ratifyOrder
函数,这为 Seaport 应用提供了对事务进展方式提出异议的机会。如果从 Seaport 应用的角度来看,转移过程中出现了问题,Seaport 应用合约有机会将回滚传递给 Seaport,这将回滚整个 fulfillOrder
函数调用。_assertRestrictedAdvancedOrderValidity
和其他检查均通过,_validateOrderAndUpdateStatus
会发出 OrderFulfilled
事件,并返回 true
至 fulfillOrder
,后者又返回 true
,与其他订单类型一样。这是一个简化的代码示例,展示了上面示例中的 Seaport 应用合约可能的样子:
import {
ContractOffererInterface
} from "../interfaces/ContractOffererInterface.sol";
import { ItemType } from "../lib/ConsiderationEnums.sol";
import {
ReceivedItem,
Schema,
SpentItem
} from "../lib/ConsiderationStructs.sol";
/**
* @title ExampleContractOfferer
* @notice ExampleContractOfferer 是一个 Seaport 应用
* 合约的伪代码草图,用于一次以 10 或更多 ETH 出售一个 Blitmaps NFT。
*/
contract ExampleContractOfferer is ContractOffererInterface {
error OrderUnavailable();
address private immutable _SEAPORT;
address private immutable _BLITMAPS;
constructor(address seaport, address blitmaps) {
_SEAPORT = seaport;
_BLITMAPS = blitmaps;
}
receive() external payable {}
function generateOrder(
address,
SpentItem[] calldata originalOffer,
SpentItem[] calldata originalConsideration,
bytes calldata /* context */
)
external
virtual
override
returns (SpentItem[] memory offer, ReceivedItem[] memory consideration)
{
SpentItem memory _originalOffer = originalOffer[0];
SpentItem memory _originalConsideration = originalConsideration[0];
if (
// 确保原始提示是寻找 Blitmaps 项目。
(_originalOffer.token == _BLITMAPS && _originalOffer.amount == 1) &&
// 确保原始提示愿意花费 10 ETH。
(_originalConsideration.amount >= 10 ether)
) {
// 设置在部署期间提供的报价和考虑。
offer = new SpentItem[](1);
consideration = new ReceivedItem[](1);
offer[0] = _originalOffer;
consideration[0] = ReceivedItem({
itemType: ItemType.NATIVE,
token: address(0),
identifier: 0,
amount: 10 ether,
recipient: payable(address(this))
});
} else {
revert OrderUnavailable();
}
}
function previewOrder(
address /* caller */,
address,
SpentItem[] calldata,
SpentItem[] calldata,
bytes calldata /* context */
)
external
view
override
returns (SpentItem[] memory offer, ReceivedItem[] memory consideration)
{
// 显示基于某些参数的订单应是什么样子。
// 应该与 `generateOrder` 所生成的订单匹配。
SpentItem[] memory _offer;
ReceivedItem[] memory _consideration;
return (_offer, _consideration);
}
function ratifyOrder(
SpentItem[] calldata /* offer */,
ReceivedItem[] calldata /* consideration */,
bytes calldata /* context */,
bytes32[] calldata /*orderHashes*/,
uint256 /* contractNonce */
)
external
pure
virtual
override
returns (bytes4 /* ratifyOrderMagicValue */)
{
// 如有需要,可以在此执行一些后续执行验证。
return ContractOffererInterface.ratifyOrder.selector;
}
/**
* @dev 返回此合约报价者的元数据。
*/
function getSeaportMetadata()
external
pure
override
returns (
string memory name,
Schema[] memory schemas // 映射到 Seaport 改进提案 ID
)
{
schemas = new Schema[](1);
schemas[0].id = 1337;
schemas[0].metadata = new bytes(0);
return ("ExampleContractOfferer", schemas);
}
}
请记得为任何新颖的 Seaport 应用创建一个 Seaport 改进提案 (SIP)。
Seaport v1.2 引入了批量订单创建功能。简而言之,买家或卖家现在可以签署单个批量订单有效负载,以使用一个 ECDSA 签名创建多个订单。因此,用户现在可以通过在他们的钱包 UI 中单击一次来创建同样数量的订单,而不必逐一签署十几个单独的订单有效负载。
从 v1.2 开始,深度为 1(2 个订单)到深度为 24(16,777,216 个订单)的批量签名有效负载得到了完全支持。就像单个订单签名一样,批量订单有效负载将是类型化的人类可读的 EIP 712 数据。在批量订单创建过程中创建的任何单个订单都可以独立履行。换句话说,在批量订单创建过程中创建的一个订单或多个订单可以包含在一个履行交易中。
请注意,履行在批量订单创建过程中创建的订单时,其 Gas 成本会增加。随着批量订单有效负载中订单数量的增加,成本呈对数增长:高度为 1 时约 4,000 gas,随后每增加一点高度约增加 700 gas。因此,建议平衡一次创建多个订单的便利性与对履行者施加的额外 Gas 成本之间的关系。
请注意,在 v1.2 中,incrementCounter
函数被修改为通过来自最后区块哈希的准随机值递增计数器。此更改防止了用户被欺骗签署恶意批量签名有效负载的情况,其中包含在当前计数器值和未来计数器值下可履行的订单,如果计数器仍然是串行递增的,这种情况是可能的。相反,由于计数器跳跃了一个非常大、准随机的数量,恶意签名的影响仍然可以通过单次递增计数器来抵消。换句话说,对 incrementCounter
的更改使买家和卖家能够在不知情的情况下“强制重置”,无论在大规模恶意批量订单有效负载中签署了什么订单。
请注意,在批量订单创建过程中创建的订单仍然需要单独取消。例如,如果创建者在单个批量订单有效负载中创建了 4 个订单,取消这 4 个订单将需要 4 次 cancel
交易。或者,创建者可以调用 incrementCounter
一次,但这也将导致其所有其他活动订单变成不可履行。用户在使用批量订单创建时应谨慎创建大量订单,并应尽量定期创建短期订单,而不是偶尔创建长期订单。
批量签名是一个 EIP 712 类型的默克尔树,其中根是 BulkOrder
,叶子是 OrderComponents
。每一层将是两个订单或一个订单和一个数组。每一层都将被哈希到树中,直到所有内容都合并到一个哈希中,待签名。对卷起哈希的签名是本文中提到的 ECDSA 签名。
一个市场可以使用签名与整个订单集结合使用(以履行整个订单集),或者允许创建者迭代每个订单,设置适当的键,并为每个订单计算证明。然后,每个证明将附加到 ECDSA 签名的末尾,使履行者能够从批量签名有效负载中选择一个或多个特定订单。有关更详细的信息,请参见以下内容。
由于批量订单有效负载的默克尔树结构和 EIP 712 的限制,每个有效负载必须包含确切的 2^N 个订单,其中 1 ≤ N ≤ 24。如果所需的签名订单数量不是一个可允许的值,则必须提供空订单(因此不可履行)以使订单总数达到可接受值(4、8、16、32 等)。换句话说,你可以创建 2 到 2^24 之间的任意数量的订单,但批量签名有效负载需要用虚拟订单进行填充。虚拟订单需要存在并具有正确的“形状”,以使批量签名有效负载与 EIP 712 配合良好,但它们不应产生任何其他效果,并且不应被执行。有关用空订单填充的批量签名有效负载的示例,请参见 the signSparseBulkOrder
function 在 Seaport Foundry 测试中。
下面是一个卖家想要一次列出 9 个不同 NFT 的批量订单有效负载的示例图:
有效的批量签名长度应大于或等于 99(1 x 32 + 67),小于或等于 836(24 x 32 + 68),并应满足以下公式:((length - 67) % 32) ≤ 1,因为每个证明应为 32 字节长。前面提到的 67 和 68 个字节是由一个 64 或 65 字节的 ECDSA 签名和一个 3 字节的索引组成。换句话说,有效批量签名的配方是:
一个 64 或 65 字节的 ECDSA 签名
+ 一个三字节索引
+ 一系列最多 24 个证明元素的 32 字节长
如果一个批量订单有效负载包含 4 个订单,则每个订单都有一个唯一的“批量签名”,其中 1) 批量签名的开头对于每个订单都是相同的 ECDSA 签名,然后 2) 每个订单都有一个唯一的索引(0-3),具体取决于该签名是针对批量订单有效负载中的哪个订单,接着 3) 每个订单都有一系列不同的证明。
例如:
ECDSA sig | index | proof 1 | proof 2 | proof 3 | proof 4 |
---|---|---|---|---|---|
0x95eb…3e9a | 000000 | 4a…e1 | 9d…3f | 7b…0c | 2d…5b |
0x95eb…3e9a | 000001 | 4a…e1 | 9d…3f | 7b…0c | 2d…5b |
0x95eb…3e9a | 000002 | 4a…e1 | 9d…3f | 7b…0c | 2d…5b |
0x95eb…3e9a | 000003 | 4a…e1 | 9d…3f | 7b…0c | 2d…5b |
这种结构允许履行者忽略签名是针对批量订单的事实。履行者只需选择完整的批量签名,该签名的索引为他们想要履行的订单,并作为单个订单的裸签名传入。Seaport 处理批量签名的解析,将其分解为组成部分,并允许履行者仅履行他们所针对的订单。
在 JavaScript 中,bulkOrderType
定义如下:
const bulkOrderType = {
BulkOrder: [{ name: "tree", type: "OrderComponents[2][2][2][2][2][2][2]" }],
OrderComponents: [
{ name: "offerer", type: "address" },
{ name: "zone", type: "address" },
{ name: "offer", type: "OfferItem[]" },
{ name: "consideration", type: "ConsiderationItem[]" },
{ name: "orderType", type: "uint8" },
{ name: "startTime", type: "uint256" },
{ name: "endTime", type: "uint256" },
{ name: "zoneHash", type: "bytes32" },
{ name: "salt", type: "uint256" },
{ name: "conduitKey", type: "bytes32" },
{ name: "counter", type: "uint256" },
],
OfferItem: [
{ name: "itemType", type: "uint8" },
{ name: "token", type: "address" },
{ name: "identifierOrCriteria", type: "uint256" },
{ name: "startAmount", type: "uint256" },
{ name: "endAmount", type: "uint256" },
],
ConsiderationItem: [
{ name: "itemType", type: "uint8" },
{ name: "token", type: "address" },
{ name: "identifierOrCriteria", type: "uint256" },
{ name: "startAmount", type: "uint256" },
{ name: "endAmount", type: "uint256" },
{ name: "recipient", type: "address" },
],
};
因此,JavaScript 中的一个示例批量订单对象可能如下所示:
const bulkOrder = {
name: "tree",
type: "OrderComponents[2][2][2][2][2][2][2]",
BulkOrder: [{
offerer: "0x123...",
zone: "0x456...",
offer: [{
itemType: 1,
token: "0x789...",
identifierOrCriteria: 123456,
startAmount: 100,
endAmount: 200
}],
consideration: [{
itemType: 2,
token: "0xabc...",
identifierOrCriteria: 789012,
startAmount: 1,
endAmount: 1,
recipient: "0xdef..."
}],
orderType: 0,
startTime: 1546300800,
endTime: 1546387199,
zoneHash: "0x9abcdef...",
salt: 123456,
conduitKey: "0xabcdef...",
counter: 789012345678901234
},
{
offerer: "0x987...",
zone: "0x654...",
offer: [{
itemType: 1,
token: "0x321...",
identifierOrCriteria: 654321,
startAmount: 150,
endAmount: 250
}],
consideration: [{
itemType: 2,
token: "0xcba...",
identifierOrCriteria: 987654,
startAmount: 1,
endAmount: 1,
recipient: "0xfed..."
}],
orderType: 1,
startTime: 1547300800,
endTime: 1547387199,
zoneHash: "0x1abcdef...",
salt: 987654,
conduitKey: "0x1abcdef...",
counter: 789012345678901234
}]
};
因此,创建批量签名可能如下所示:
const signature = _signTypedData(
domainData,
bulkOrderType,
value
);
其中,domainData
与单个订单相同,bulkOrderType
如上所示定义,value 是 OrderComponents
的树,如上所示。有关实现示例,请参见 the signBulkOrder
function 在 seaport-js 中。
请再次注意,支持批量订单的市场的繁重工作发生在创建者签名的创建端。在接受者那边,履行者能够将批量签名传入,就像它是一个普通订单的签名一样。为了完整性和一般兴趣,以下两段提供了 Seaport 内部解析批量签名的草图。
在处理签名时,Seaport 将首先检查签名是否是批量签名(64 或 65 字节的 ECDSA 签名,后面跟着一个三字节索引,随后是额外的证明元素)。然后,Seaport 将删除额外的数据以创建新的摘要,并根据通常的代码路径正常处理剩余的 64 或 65 字节的 ECDSA 签名。
换句话说,如果 _isValidBulkOrderSize
返回 true,Seaport 将使用传递给 _verifySignature
的完整 signature
和 orderHash
调用 _computeBulkOrderProof
以生成修剪后的 ECDSA 签名和相关的 bulkOrderHash
。然后,_deriveEIP712Digest
创建相关的摘要。从此以后,Seaport 将如同往常一样处理摘要和 ECDSA 签名,从 _assertValidSignature
开始。
isValidOrder
的限制订单,实际要求订单履行的路由,因此可以执行履行后验证。另一种有趣的解决此问题的方法是“以反击之火”,要求出价者在要求额外保证的订单上包含一个“验证者” ERC1155 考量项;这是一个包含 ERC1155 接口,但实际上并不是 1155 代币的合约,而是利用 onReceived
钩子来验证预期不变性得以维持,如果基于检查失败则回滚“转移”(因此在上述示例中,此钩子将确保出价者是相关 ERC721 项目的所有者,并且尚未用于铸造其他 ERC721 的商品)。此机制的关键限制是可以通过此途径以带内形式提供的数据量;仅有三个参数(“from”,“identifier”和“amount”)可供利用。validateOrder
存在它们,因为考量项可以任意扩展,重要的警告是未支出的出价项数量不得超过原始出价项数量。matchOrders
,为履行者提供一个相反的订单,明确指定允许支出的最大出价项目数量和接收的考量项目数量。在处理同时包含短期持续时间和升序或降序金额的订单时要特别小心,因为实现的数量可能在短时间窗口内显著变化。ERC721_TO_ETH
和 ERC1155_TO_ETH
基本订单路由类型。此机制的一个重要启示是,技术上讲,任何人都可以代表给定出价者提供以太(而出价者自己必须提供所有其他项目)。这还意味着所有以太必须在原始调用订单或订单组时提供(且出价项目可支出的金额在执行期间不能通过外部来源增加,如代币余额那样)。validate
的调用)或部分填充的订单在随后的履行中将跳过签名验证,使用 EIP-1271 验证订单的订单可能最终处于不一致状态,其中原始签名不再有效,但仍然可履行。在这些情况下,如果出价者不再希望该订单可履行,则必须明确撤销先前验证的订单。我是 AI 翻译助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!