Seaport - Order

Seaport 是 OpenSea 下一代交易协议。
本文讨论 Seaport 版本 1.1.0 中 Order 实体/ Order Hash/ Order 签名过程以及 Order 创建,为下一篇 fulfill order 做准备。

1. Order 以及 AdvancedOrder 的数据结构

Order

订单实体

1
2
3
4
5
6
struct Order {
// 订单参数
OrderParameters parameters;
// 订单签名
bytes signature;
}

订单参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct OrderParameters {
// 报价人,供应内容放在 offer 中
address offerer; // 0x00
// 辅助账户。在严格交易模式下,交易的发起者应当是 offerer、zone 或者 zone 的授权账户(通过 EIP1271验证)
// 这个账户的第二个作用在于,它可以取消交易
address zone; // 0x20
// offerer 账户中要转移的对象
OfferItem[] offer; // 0x40
// 对价
ConsiderationItem[] consideration; // 0x60
// 是否可以部分交易;是否任何人都可以执行订单的调用
OrderType orderType; // 0x80 订单类型
// 订单被激活时的区块上时间戳
uint256 startTime; // 0xa0
// 订单结束时的区块上时间戳
uint256 endTime; // 0xc0
// EIP1271 验证时需要的字段
bytes32 zoneHash; // 0xe0
// 熵增
uint256 salt; // 0x100
bytes32 conduitKey; // 0x120
// 交易需要满足 offerer 的 counter
uint256 totalOriginalConsiderationItems; // 0x140
// offer.length // 0x160
}

订单参数中,offer 数组的元素类型为 OfferItem,表示可从 offerer 账户中转移的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct OfferItem {
// 0: ETH on mainnet, MATIC on polygon, etc. 1: ERC20 items 2: ERC721 items
// 3: ERC1155 items 4: ERC721 items where a number of tokenIds are supported
// 5: ERC1155 items where a number of ids are supported
ItemType itemType;
// item token 合约地址
address token;
// erc721/erc1155 token id;对于 criteria-based item,是 merkle root;对于 ERC20 或者 Ether,该值没有意义
uint256 identifierOrCriteria;
// 开始时的金额
uint256 startAmount;
// 结束时的金额,从开始到结束的金额,是线性变化的
uint256 endAmount;
}

订单参数中,consideration 数组中元素的类型为 ConsiderationItem,表示与 Offer 相对应的对价。

1
2
3
4
5
6
7
8
9
10
struct ConsiderationItem {
ItemType itemType;
address token;
uint256 identifierOrCriteria;
uint256 startAmount;
uint256 endAmount;
// 注意,和 OfferItem 不同,这里多了一个 recipient。
// 对应着 item 的接收地址
address payable recipient;
}

Order 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
enum OrderType {
// 0: no partial fills, anyone can execute
FULL_OPEN,

// 1: partial fills supported, anyone can execute
PARTIAL_OPEN,

// 2: no partial fills, only offerer or zone can execute
FULL_RESTRICTED,

// 3: partial fills supported, only offerer or zone can execute
PARTIAL_RESTRICTED
}

Order 状态

1
2
3
4
5
6
struct OrderStatus {
bool isValidated;
bool isCancelled;
uint120 numerator;
uint120 denominator;
}

AdvancedOrder

高级订单中,不仅包括了 Order,还包括了 numeratordenominator

1
2
3
4
5
6
7
8
9
struct AdvancedOrder {
OrderParameters parameters;
// a fraction to attempt to fill
uint120 numerator;
// the total size of the order
uint120 denominator;
bytes signature;
bytes extraData;
}

对于 numeratordenominator 两个参数其实是不易让人理解的,不过,可以通过这两个参数的具体应用来了解它们,可以看到其主要用于 amount 的计算过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// AmountDeriver.sol
// 如果是 ether 或者其它 erc20,返回的 amount 代表金额;
// 如果 item 基于 erc721 协议,返回的 amount 应当是 1;
// 如果 item 基于 erc1155 协议,返回的 amount 代表的是某个 nft 的数量
// @param startAmount,订单初始价格;
// @param endAmount,订单结束时的价格;
// @param numberator 分子;
// @param denominator 分母;
// numberator/denominator 实际上代表了在交易内容符合 erc1155 或者 erc20 协议时,交换者希望兑换的某一对象的份额
// @param startTime 订单开始的时间;
// @param endTime 订单结束的时间;
// @param roundUp 结果是向上还是向下取整
function _applyFraction(
uint256 startAmount,
uint256 endAmount,
uint256 numerator,
uint256 denominator,
uint256 startTime,
uint256 endTime,
bool roundUp
) internal view returns (uint256 amount) {
// 固定价格
if (startAmount == endAmount) {
// Apply fraction to end amount.
amount = _getFraction(numerator, denominator, endAmount);
} else {
// 基于数量和时间的浮动价格
amount = _locateCurrentAmount(
_getFraction(numerator, denominator, startAmount),
_getFraction(numerator, denominator, endAmount),
startTime,
endTime,
roundUp
);
}
}

function _getFraction(
uint256 numerator,
uint256 denominator,
uint256 value
) internal pure returns (uint256 newValue) {
// 分子分母相同,不存在比例问题
if (numerator == denominator) {
return value;
}

// that the denominator cannot be zero.
assembly {
// 确保 (value * numerator) % denominator 没有剩余
// 没有 半个 erc1155 nft;也没有半个 erc20,对于 erc20 已经是最小单位了。
// 数量均无法再次分割。
if mulmod(value, numerator, denominator) {
mstore(0, InexactFraction_error_signature)
revert(0, InexactFraction_error_len)
}
}

// Multiply the numerator by the value and ensure no overflow occurs.
uint256 valueTimesNumerator = value * numerator;

// 所以,实际上就是 :value = value * numerator / denominator
assembly {
// 这里没有对 denominator 是否为 0 做判断
newValue := div(valueTimesNumerator, denominator)
}
}

function _locateCurrentAmount(
uint256 startAmount,
uint256 endAmount,
uint256 startTime,
uint256 endTime,
bool roundUp
) internal view returns (uint256 amount) {
// Only modify end amount if it doesn't already equal start amount.
if (startAmount != endAmount) {
// 订单有效时间长度
uint256 duration;
// 订单已经过去的时间
uint256 elapsed;
// 剩余的订单有效时长
uint256 remaining;

unchecked {
duration = endTime - startTime;
elapsed = block.timestamp - startTime;
remaining = duration - elapsed;
}

// totalBeforeDivision/duration 得到最终值
uint256 totalBeforeDivision = ((startAmount * remaining) +
(endAmount * elapsed));

// Use assembly to combine operations and skip divide-by-zero check.
assembly {
// Multiply by iszero(iszero(totalBeforeDivision)) to ensure
// amount is set to zero if totalBeforeDivision is zero,
// as intermediate overflow can occur if it is zero.
amount := mul(
// iszero(x) 1 if x == 0, 0 otherwise
iszero(iszero(totalBeforeDivision)),
// 取整策略
add(
div(sub(totalBeforeDivision, roundUp), duration),
roundUp
)
)
}
// Return the current amount.
return amount;
}

// Return the original amount as startAmount == endAmount.
return endAmount;
}

实际的交易过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
for (uint256 i = 0; i < totalOfferItems; ++i) {
...
// 获取当前检索的 OfferItem
OfferItem memory offerItem = orderParameters.offer[i];
uint256 amount = _applyFraction(
offerItem.startAmount,
offerItem.endAmount,
numerator,
denominator,
startTime,
endTime,
false
);
...
// 价值转移
_transferOfferItem(
offerItem, // item
orderParameters.offerer, // from
orderParameters.conduitKey, // conduitKey
accumulator // accumulator
);
...
}

根据不同的 ItemType,结合 amount,完成资产转移:

1
2
3
4
5
6
7
8
9
10
// 在 _transferOfferItem 执行体内完成最后的价值转移 

// 如果 ItemType 为 ItemType.NATIVE
_transferEth(item.recipient, item.amount);
// 如果 ItemType 为 ItemType.ERC20
_transferERC20(item.token,from,item.recipient,item.amount,conduitKey, accumulator);
// 如果 ItemType 为 ItemType.ERC721,这种情况下,amount 应为 1
_transferERC721(item.token,from,item.recipient,item.identifier,item.amount,conduitKey,accumulator);
// 如果 ItemType 为 ItemType.ERC1155
_transferERC1155( item.token, from,item.recipient,item.identifier, item.amount,conduitKey,accumulator);

2. 订单 Hash

ts 版本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
export const calculateOrderHash = (orderComponents: OrderComponents) => {
const offerItemTypeString =
"OfferItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount)";
const considerationItemTypeString =
"ConsiderationItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount,address recipient)";
const orderComponentsPartialTypeString =
"OrderComponents(address offerer,address zone,OfferItem[] offer,ConsiderationItem[] consideration,uint8 orderType,uint256 startTime,uint256 endTime,bytes32 zoneHash,uint256 salt,bytes32 conduitKey,uint256 counter)";
const orderTypeString = `${orderComponentsPartialTypeString}${considerationItemTypeString}${offerItemTypeString}`;

const offerItemTypeHash = keccak256(toUtf8Bytes(offerItemTypeString));
const considerationItemTypeHash = keccak256(
toUtf8Bytes(considerationItemTypeString)
);
const orderTypeHash = keccak256(toUtf8Bytes(orderTypeString));

// 计算 offer 的 Hash
const offerHash = keccak256(
"0x" +
orderComponents.offer
.map((offerItem) => {
return keccak256(
"0x" +
[
offerItemTypeHash.slice(2),
offerItem.itemType.toString().padStart(64, "0"),
offerItem.token.slice(2).padStart(64, "0"),
toBN(offerItem.identifierOrCriteria)
.toHexString()
.slice(2)
.padStart(64, "0"),
toBN(offerItem.startAmount)
.toHexString()
.slice(2)
.padStart(64, "0"),
toBN(offerItem.endAmount)
.toHexString()
.slice(2)
.padStart(64, "0"),
].join("")
).slice(2);
})
.join("")
);

// 计算 consideration 的 Hash
const considerationHash = keccak256(
"0x" +
orderComponents.consideration
.map((considerationItem) => {
return keccak256(
"0x" +
[
considerationItemTypeHash.slice(2),
considerationItem.itemType.toString().padStart(64, "0"),
considerationItem.token.slice(2).padStart(64, "0"),
toBN(considerationItem.identifierOrCriteria)
.toHexString()
.slice(2)
.padStart(64, "0"),
toBN(considerationItem.startAmount)
.toHexString()
.slice(2)
.padStart(64, "0"),
toBN(considerationItem.endAmount)
.toHexString()
.slice(2)
.padStart(64, "0"),
considerationItem.recipient.slice(2).padStart(64, "0"),
].join("")
).slice(2);
})
.join("")
);

// ① Hash( 数据类型 Hash + 数据内容)。
// ② 如果数据内容是数组,则 Hash(拼接(数据类型 Hash + 数据内容(i)))
// ③ 如果数据内容中包含复合结构,则通过 ① 或者 ② 获取到该数据项的 Hash 代替该数据
const derivedOrderHash = keccak256(
"0x" +
[
orderTypeHash.slice(2), // 数据类型的 Hash
orderComponents.offerer.slice(2).padStart(64, "0"),
orderComponents.zone.slice(2).padStart(64, "0"),
offerHash.slice(2),
considerationHash.slice(2),
orderComponents.orderType.toString().padStart(64, "0"),
toBN(orderComponents.startTime)
.toHexString()
.slice(2)
.padStart(64, "0"),
toBN(orderComponents.endTime).toHexString().slice(2).padStart(64, "0"),
orderComponents.zoneHash.slice(2),
orderComponents.salt.slice(2).padStart(64, "0"),
orderComponents.conduitKey.slice(2).padStart(64, "0"),
toBN(orderComponents.counter).toHexString().slice(2).padStart(64, "0"),
].join("")
);

return derivedOrderHash;
};

sol 版本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
function _deriveOrderHash(
OrderParameters memory orderParameters,
uint256 counter
) internal view returns (bytes32 orderHash) {
// Get length of original consideration array and place it on the stack.
uint256 originalConsiderationLength = (
orderParameters.totalOriginalConsiderationItems
);

/*
* Memory layout for an array of structs (dynamic or not) is similar
* to ABI encoding of dynamic types, with a head segment followed by
* a data segment. The main difference is that the head of an element
* is a memory pointer rather than an offset.
*/
// Declare a variable for the derived hash of the offer array.
bytes32 offerHash;

// Read offer item EIP-712 typehash from runtime code & place on stack.
bytes32 typeHash = _OFFER_ITEM_TYPEHASH;

// Utilize assembly so that memory regions can be reused across hashes.
assembly {
// FreeMemoryPointerSlot = 0x40;
// m:=mload(0x40) instruction reads the 32 bytes of memory starting at position 0x40.
// Retrieve the free memory pointer and place on the stack.
let hashArrPtr := mload(FreeMemoryPointerSlot)
// Get the pointer to the offers array.
let offerArrPtr := mload(
// OrderParameters_offer_head_offset = 0x40;
// 跨过前面的 offerer 和 zone,这两个数据都是 address 类型,占 64 字节
// 之后,就是 offer 数组
add(orderParameters, OrderParameters_offer_head_offset)
)
// Load the length.数组的长度
let offerLength := mload(offerArrPtr)
// Set the pointer to the first offer's head.
// OneWord = 0x20;
// offer 数组的 OneWord,表示数组的长度
// 跨过 OneWord,则达到实际数组存储的位置
offerArrPtr := add(offerArrPtr, OneWord)
// Iterate over the offer items.prettier-ignore
// https://solidity-cn.readthedocs.io/zh/develop/assembly.html#id11
// 内联汇编中的 for 循环:初始化部分、条件和迭代后处理部分. lt(x, y) 如果 x < y 为 1,否则为 0
for { let i := 0 } lt(i, offerLength) {
i := add(i, 1)
} {
// Read the pointer to the offer data and subtract one word to get typeHash pointer.
// mload(p) -> mem[p...(p + 32))]
let ptr := sub(mload(offerArrPtr), OneWord)
// Read the current value before the offer data. mload(p) -> mem[p...(p + 32))]
// 这里临时解用 ptr 所指向的 32 位空间,后面再把 value 还原回去
let value := mload(ptr)
// Write the type hash to the previous word. mstore(p, v) -> mem[p...(p + 32)] := v
mstore(ptr, typeHash)
// Take the EIP712 hash and store it in the hash array. EIP712_OfferItem_size = 0xc0;
// 在 hashAddrPtr 上存储单个 item 的哈希值
mstore(hashArrPtr, keccak256(ptr, EIP712_OfferItem_size))
// Restore the previous word.
// 我们临时借用了 ptr 出的 32 位空间,现在把它还回去 !!!
mstore(ptr, value)
// Increment the array pointers by one word.
offerArrPtr := add(offerArrPtr, OneWord)
// hashArrPtr 前移 32 个位
hashArrPtr := add(hashArrPtr, OneWord)
}

// Derive the offer hash using the hashes of each item.
offerHash := keccak256(
mload(FreeMemoryPointerSlot),
mul(offerLength, OneWord)
)
}

// Declare a variable for the derived hash of the consideration array.
bytes32 considerationHash;

// Read consideration item typehash from runtime code & place on stack.
typeHash = _CONSIDERATION_ITEM_TYPEHASH;

// Utilize assembly so that memory regions can be reused across hashes.
assembly {
// Retrieve the free memory pointer and place on the stack.
let hashArrPtr := mload(FreeMemoryPointerSlot)

// Get the pointer to the consideration array.
let considerationArrPtr := add(
mload(
add(
orderParameters,
OrderParameters_consideration_head_offset
)
),
OneWord
)

// Iterate over the consideration items (not including tips).
// prettier-ignore
for { let i := 0 } lt(i, originalConsiderationLength) {
i := add(i, 1)
} {
// Read the pointer to the consideration data and subtract one
// word to get typeHash pointer.
let ptr := sub(mload(considerationArrPtr), OneWord)

// Read the current value before the consideration data.
let value := mload(ptr)

// Write the type hash to the previous word.
mstore(ptr, typeHash)

// Take the EIP712 hash and store it in the hash array.
mstore(
hashArrPtr,
keccak256(ptr, EIP712_ConsiderationItem_size)
)

// Restore the previous word.
mstore(ptr, value)

// Increment the array pointers by one word.
considerationArrPtr := add(considerationArrPtr, OneWord)
hashArrPtr := add(hashArrPtr, OneWord)
}

// Derive the consideration hash using the hashes of each item.
considerationHash := keccak256(
mload(FreeMemoryPointerSlot),
mul(originalConsiderationLength, OneWord)
)
}

// Read order item EIP-712 typehash from runtime code & place on stack.
typeHash = _ORDER_TYPEHASH;

// Utilize assembly to access derived hashes & other arguments directly.
assembly {
// Retrieve pointer to the region located just behind parameters.
let typeHashPtr := sub(orderParameters, OneWord)

// Store the value at that pointer location to restore later.
let previousValue := mload(typeHashPtr)

// Store the order item EIP-712 typehash at the typehash location.
mstore(typeHashPtr, typeHash)

// Retrieve the pointer for the offer array head.
let offerHeadPtr := add(
orderParameters,
OrderParameters_offer_head_offset
)

// Retrieve the data pointer referenced by the offer head.
let offerDataPtr := mload(offerHeadPtr)

// Store the offer hash at the retrieved memory location.
mstore(offerHeadPtr, offerHash)

// Retrieve the pointer for the consideration array head.
let considerationHeadPtr := add(
orderParameters,
OrderParameters_consideration_head_offset
)

// Retrieve the data pointer referenced by the consideration head.
let considerationDataPtr := mload(considerationHeadPtr)

// Store the consideration hash at the retrieved memory location.
mstore(considerationHeadPtr, considerationHash)

// Retrieve the pointer for the counter.
let counterPtr := add(
orderParameters,
OrderParameters_counter_offset
)

// Store the counter at the retrieved memory location.
mstore(counterPtr, counter)

// Derive the order hash using the full range of order parameters.
// 返回 orderHash
orderHash := keccak256(typeHashPtr, EIP712_Order_size)

// Restore the value previously held at typehash pointer location.
mstore(typeHashPtr, previousValue)

// Restore offer data pointer at the offer head pointer location.
mstore(offerHeadPtr, offerDataPtr)

// Restore consideration data pointer at the consideration head ptr.
mstore(considerationHeadPtr, considerationDataPtr)

// Restore consideration item length at the counter pointer.
mstore(counterPtr, originalConsiderationLength)
}
}

3. Order Signature

ts 版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const signOrder = async (
orderComponents: OrderComponents,
signer: Wallet
) => {
const signature = await signer._signTypedData(
domainData,
orderType,
orderComponents
);
// 本地计算出来的 Hash 应该与 合约计算出来的 Hash 一致。
const orderHash = await getAndVerifyOrderHash(orderComponents);

const { domainSeparator } = await marketplaceContract.information();
const digest = keccak256(
`0x1901${domainSeparator.slice(2)}${orderHash.slice(2)}`
);
const recoveredAddress = recoverAddress(digest, signature);

expect(recoveredAddress).to.equal(signer.address);

return signature;
};

4. Order Create

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// createOrder start
const createOrder = async (
offerer: Wallet,
zone: Wallet | undefined | string = undefined,
offer: OfferItem[],
consideration: ConsiderationItem[],
orderType: number,
criteriaResolvers?: CriteriaResolver[],
timeFlag?: string | null,
signer?: Wallet,
zoneHash = constants.HashZero,
conduitKey = constants.HashZero,
extraCheap = false
) => {
// 获取 offerer 当前 counter
const counter = await marketplaceContract.getCounter(offerer.address);
// 获取盐值
const salt = !extraCheap ? randomHex() : constants.HashZero;
const startTime =
timeFlag !== "NOT_STARTED" ? 0 : toBN("0xee00000000000000000000000000");
const endTime =
timeFlag !== "EXPIRED" ? toBN("0xff00000000000000000000000000") : 1;

const orderParameters = {
offerer: offerer.address,
zone: !extraCheap
? (zone as Wallet).address || (zone as string)
: constants.AddressZero,
offer,
consideration,
totalOriginalConsiderationItems: consideration.length,
orderType,
zoneHash,
salt,
conduitKey,
startTime,
endTime,
};

const orderComponents = {
...orderParameters,
counter,
};

const orderHash = await getAndVerifyOrderHash(orderComponents);

const { isValidated, isCancelled, totalFilled, totalSize } =
await marketplaceContract.getOrderStatus(orderHash);

expect(isCancelled).to.equal(false);

const orderStatus = {
isValidated,
isCancelled,
totalFilled,
totalSize,
};

const flatSig = await signOrder(orderComponents, signer || offerer);

const order = {
parameters: orderParameters,
signature: !extraCheap ? flatSig : convertSignatureToEIP2098(flatSig),
numerator: 1, // only used for advanced orders
denominator: 1, // only used for advanced orders
extraData: "0x", // only used for advanced orders
};

// How much ether (at most) needs to be supplied when fulfilling the order
const value = offer
.map((x) =>
x.itemType === 0
? x.endAmount.gt(x.startAmount)
? x.endAmount
: x.startAmount
: toBN(0)
)
.reduce((a, b) => a.add(b), toBN(0))
.add(
consideration
.map((x) =>
x.itemType === 0
? x.endAmount.gt(x.startAmount)
? x.endAmount
: x.startAmount
: toBN(0)
)
.reduce((a, b) => a.add(b), toBN(0))
);

return {
order,
orderHash,
value,
orderStatus,
orderComponents,
};
};

参考链接

Seaport Overview
源码
SeaPort 在 Rinkey 的部署
What is the purpose of “unchecked” in Solidity?

© 2024 YueGS