-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathCollectionBatchBuyOperator.sol
224 lines (189 loc) · 8.26 KB
/
CollectionBatchBuyOperator.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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.17;
import "./IOperator.sol";
import "../party/Party.sol";
import "../tokens/IERC721.sol";
import "../utils/LibRawResult.sol";
import "../utils/LibAddress.sol";
import "../utils/LibSafeERC721.sol";
import "openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
/// @notice A crowdfund that purchases a specific NFT (i.e., with a known token
/// ID) listing for a known price.
contract CollectionBatchBuyOperator is IOperator {
using LibRawResult for bytes;
using LibSafeERC721 for IERC721;
using LibAddress for address payable;
struct CollectionBatchBuyOperationData {
/// The contract of NFTs to buy.
IERC721 nftContract;
/// The merkle root of the token IDs that can be bought. If null,
/// allow any token ID in the collection can be bought.
bytes32 nftTokenIdsMerkleRoot;
// Maximum amount this crowdfund will pay for an NFT.
uint256 maximumPrice;
// Minimum number of tokens that must be purchased. If this limit is
// not reached, the batch buy will fail.
uint256 minTokensBought;
// Minimum amount of ETH that must be used to buy the tokens. If this
// amount is not reached, the batch buy will fail.
uint256 minTotalEthUsed;
}
struct TokenToBuy {
// The token ID of the NFT to buy.
uint256 tokenId;
// The price of the token. This cannot be greater than `maximumPrice`.
uint96 price;
// The proof needed to verify that the token ID is included in the
// `nftTokenIdsMerkleRoot` (if it is not null).
bytes32[] proof;
}
struct BuyCall {
// The contract to call to buy the NFTs in `tokensToBuy`.
address payable target;
// The calldata to call `target` with to buy the NFTs in `tokensToBuy`.
bytes data;
// The tokens to try buying with this call.
TokenToBuy[] tokensToBuy;
}
struct CollectionBatchBuyExecutionData {
// The calls made to buy the NFTs. Each call has a target, data, and
// the tokens to buy in that call.
BuyCall[] calls;
// The total number of tokens that can be bought in this batch buy. This
// should be equal to the sum of the each `tokensToBuy` in `calls`.
uint256 numOfTokens;
}
event CollectionBatchBuyOperationExecuted(
Party party,
IERC721 token,
uint256[] tokenIdsBought,
uint256 totalEthUsed
);
error NothingBoughtError();
error InvalidMinTokensBoughtError(uint256 minTokensBought);
error InvalidTokenIdError();
error NotEnoughTokensBoughtError(uint256 tokensBought, uint256 minTokensBought);
error NotEnoughEthUsedError(uint256 ethUsed, uint256 minTotalEthUsed);
error MaximumPriceError(uint256 callValue, uint256 maximumPrice);
error CallProhibitedError(address target, bytes data);
error NumOfTokensCannotBeLessThanMin(uint256 numOfTokens, uint256 min);
error EthUsedForFailedBuyError(uint256 expectedEthUsed, uint256 actualEthUsed);
function execute(bytes memory operatorData, bytes memory executionData) external payable {
// Decode the operator data.
CollectionBatchBuyOperationData memory op = abi.decode(
operatorData,
(CollectionBatchBuyOperationData)
);
// Decode the execution data.
CollectionBatchBuyExecutionData memory ex = abi.decode(
executionData,
(CollectionBatchBuyExecutionData)
);
if (op.minTokensBought == 0) {
// Must buy at least one token.
revert InvalidMinTokensBoughtError(0);
}
if (ex.numOfTokens < op.minTokensBought) {
// The number of tokens to buy must be greater than or equal to the
// minimum number of tokens to buy.
revert NumOfTokensCannotBeLessThanMin(ex.numOfTokens, op.minTokensBought);
}
// Lengths of arrays are updated at the end.
uint256[] memory tokenIds = new uint256[](ex.numOfTokens);
uint96 totalEthUsed;
uint256 tokensBought;
for (uint256 i; i < ex.calls.length; ++i) {
BuyCall memory call = ex.calls[i];
uint96 callValue;
for (uint256 j; j < call.tokensToBuy.length; ++j) {
TokenToBuy memory tokenToBuy = call.tokensToBuy[j];
if (op.nftTokenIdsMerkleRoot != bytes32(0)) {
// Verify the token ID is in the merkle tree.
_verifyTokenId(tokenToBuy.tokenId, op.nftTokenIdsMerkleRoot, tokenToBuy.proof);
}
// Check that the call value is under the maximum price.
uint96 price = tokenToBuy.price;
if (price > op.maximumPrice) {
revert MaximumPriceError(price, op.maximumPrice);
}
// Add the price to the total value used for the call.
callValue += price;
}
uint256 balanceBefore = address(this).balance;
{
// Execute the call to buy the NFT.
(bool success, ) = _buy(call.target, callValue, call.data);
if (!success) continue;
}
{
uint96 ethUsed;
for (uint256 j; j < call.tokensToBuy.length; ++j) {
uint256 tokenId = call.tokensToBuy[j].tokenId;
uint96 price = call.tokensToBuy[j].price;
// Check whether the NFT was successfully bought.
if (op.nftContract.safeOwnerOf(tokenId) == address(this)) {
ethUsed += price;
++tokensBought;
// Add the token to the list of tokens to finalize.
tokenIds[tokensBought - 1] = tokenId;
}
}
// Check ETH spent for call is what was expected.
uint256 actualEthUsed = balanceBefore - address(this).balance;
if (ethUsed != actualEthUsed) {
revert EthUsedForFailedBuyError(ethUsed, actualEthUsed);
}
totalEthUsed += ethUsed;
}
}
// This is to prevent this crowdfund from finalizing a loss if nothing
// was attempted to be bought (ie. `tokenIds` is empty) or all NFTs were
// bought for free.
if (totalEthUsed == 0) revert NothingBoughtError();
// Check number of tokens bought is not less than the minimum.
if (tokensBought < op.minTokensBought) {
revert NotEnoughTokensBoughtError(tokensBought, op.minTokensBought);
}
// Check total ETH used is not less than the minimum.
if (totalEthUsed < op.minTotalEthUsed) {
revert NotEnoughEthUsedError(totalEthUsed, op.minTotalEthUsed);
}
assembly {
// Update length of `tokenIds`
mstore(mload(ex), tokensBought)
}
// Transfer the NFTs to the party.
for (uint256 i; i < tokenIds.length; ++i) {
op.nftContract.safeTransferFrom(address(this), msg.sender, tokenIds[i]);
}
// Transfer unused ETH to the party.
uint256 unusedEth = msg.value - totalEthUsed;
if (unusedEth > 0) payable(msg.sender).transferEth(unusedEth);
emit CollectionBatchBuyOperationExecuted(
Party(payable(msg.sender)),
op.nftContract,
tokenIds,
totalEthUsed
);
}
function _buy(
address payable callTarget,
uint96 callValue,
bytes memory callData
) private returns (bool success, bytes memory revertData) {
// Check that call is not re-entering.
if (callTarget == address(this)) {
revert CallProhibitedError(callTarget, callData);
}
// Execute the call to buy the NFT.
(success, revertData) = callTarget.call{ value: callValue }(callData);
}
function _verifyTokenId(uint256 tokenId, bytes32 root, bytes32[] memory proof) private pure {
bytes32 leaf;
assembly {
mstore(0x00, tokenId)
leaf := keccak256(0x00, 0x20)
}
if (!MerkleProof.verify(proof, root, leaf)) revert InvalidTokenIdError();
}
}