-
Notifications
You must be signed in to change notification settings - Fork 0
/
Marketplace.sol
528 lines (469 loc) · 18.3 KB
/
Marketplace.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
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.9;
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import { PullPaymentUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PullPaymentUpgradeable.sol";
import { CountersUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol";
error NFTNotApproved(IERC721 tokenContract, uint256 tokenId);
error InvalidNFTOwner(address spender);
error PurchaseForbidden(address buyer);
error WithdrawalForbidden(address payee);
error WithdrawalLocked(address payee, uint256 currentTimestamp, uint256 unlockTimestamp);
error TokenAlreadyListed(IERC721 tokenContract, uint256 tokenId);
error TokenNotListed(IERC721 tokenContract, uint256 tokenId);
error InvalidListingFee(IERC721 tokenContract, uint256 tokenId, uint256 fee);
error InvalidListingPrice(IERC721 tokenContract, uint256 tokenId, uint256 price);
error ZeroPrice();
/**
* @dev This contract implements a marketplace that allows users to sell and buy non-fungible
* tokens (NFTs) which are compliant with the ERC-721 standard. In particular the marketplace
* exposes the following functionality to its users:
* - List an NFT.
* - Delist an NFT.
* - Buy an NFT with transferring ownership.
* - Update listing data.
* - Get listing data.
*
* All the opertions above identify an NFT by the address of its NTF contract and the identifier
* assigned within this NTF contract. Note the marketplace doesn't assume NFT's ownership when the
* NFT is listed there. So it's a responsibility of the NFT owner to guarantee that the
* marketplace is approved to manage the NFT in advance. The approval is achieved by calling the
* approve method on the NFT contract with the marketplace's address and the given NFT.
*
* From the administrative perspective the contract is controlled by the single owner account. The
* owner is capable of * executing the following functionality:
* - Pause/unpause invocation of certain methods on the contract in case of emergency.
* - Transfer/Renounce the contract's ownership.
* - Set the listing fee and the withdrawal period.
*
* The contract is upgradeable by sticking to the proxy upgrade pattern based on the unstructured
* storage and transparent proxies approach. For more information, see
* https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies.
*
* The marketplace implements a pull payment strategy, where the NFT seller account doesn't receive
* its owed payments directly from the marketplace, but has to withdraw them on its own instead.
* For more detail on this strategy, see
* https://consensys.github.io/smart-contract-best-practices/development-recommendations/general/external-calls/#favor-pull-over-push-for-external-calls
*
* Note that payments cannot be withdrawn immediately. The seller have to wait for a certain
* withdrawal period starting from the moment they carried out their last trade.
*
* Some methods are made reentrancy resistant due to the fact that their implementation is unable
* to be fully compliant with the checks-effects-interractions pattern due to forced external
* calls in modifiers.
*/
contract Marketplace is
Initializable,
OwnableUpgradeable,
PausableUpgradeable,
ReentrancyGuardUpgradeable,
PullPaymentUpgradeable
{
/*///////////////////////////////////////////////////////////////
External libraries
//////////////////////////////////////////////////////////////*/
using CountersUpgradeable for CountersUpgradeable.Counter;
/*///////////////////////////////////////////////////////////////
Type declarations
//////////////////////////////////////////////////////////////*/
struct Listing {
address seller;
uint256 price;
}
/*///////////////////////////////////////////////////////////////
State variables
//////////////////////////////////////////////////////////////*/
/**
* @dev The current listing fee. It can be changed through the `setListingFee` method.
*
* @custom:security write-protection="onlyOwner()"
*/
uint256 public listingFee;
/**
* @dev The current withdrawal period. It can be changed through the `setWithdrawalPeriod`
* method.
*
* @custom:security write-protection="onlyOwner()"
*/
uint256 public withdrawalPeriod;
/**
* @dev The total listing count at the marketplace.
*/
CountersUpgradeable.Counter public listingCount;
/**
* @dev The mapping between accounts who sold an NFT and release dates of locked payments.
*/
mapping(address => uint256) public paymentDates;
/**
* @dev The mapping between an NFT (identitifed as the NFT address + NFT id) and a listing at
* the marketplace.
*/
//slither-disable-next-line uninitialized-state
mapping(IERC721 => mapping(uint256 => Listing)) private _tokenToListing;
/*///////////////////////////////////////////////////////////////
Events
//////////////////////////////////////////////////////////////*/
/**
* @dev Emitted when the `seller` account lists the NFT at the marketplace.
*/
event TokenListed(
address indexed seller,
IERC721 indexed tokenContract,
uint256 indexed tokenId,
uint256 price
);
/**
* @dev Emitted when the `seller` account delists the NFT from the marketplace.
*/
event TokenDelisted(
address indexed seller,
IERC721 indexed tokenContract,
uint256 indexed tokenId
);
/**
* @dev Emitted when the `buyer` account purchases the NFT listed at the marketplace.
*/
event TokenBought(
address indexed buyer,
IERC721 indexed tokenContract,
uint256 indexed tokenId,
uint256 price
);
/**
* @dev Emitted when the `payee` account withdraws accumulated payments.
*/
event PaymentsWithdrawn(address indexed payee, uint256 amount);
/**
* @dev Emitted when the owner sets the given listing fee.
*/
event ListingFeeSet(uint256 listingFee);
/**
* @dev Emitted when the owner sets the given withdrawal period.
*/
event WithdrawalPeriodSet(uint256 withdrawalPeriod);
/*///////////////////////////////////////////////////////////////
Modifiers
//////////////////////////////////////////////////////////////*/
/**
* @dev Throws if called by any account other than the NFT owner.
*/
modifier isNFTOwner(
IERC721 tokenContract,
uint256 tokenId,
address spender
) {
if (tokenContract.ownerOf(tokenId) != spender) {
revert InvalidNFTOwner(spender);
}
_;
}
/**
* @dev Throws if the NFT is not listed at the marketplace.
*/
modifier isListed(IERC721 tokenContract, uint256 tokenId) {
Listing memory listing = _tokenToListing[tokenContract][tokenId];
if (listing.price == 0) {
revert TokenNotListed(tokenContract, tokenId);
}
_;
}
/**
* @dev Throws if the NFT is not approved by the owner account.
*/
modifier isApproved(IERC721 tokenContract, uint256 tokenId) {
if (tokenContract.getApproved(tokenId) != address(this)) {
revert NFTNotApproved(tokenContract, tokenId);
}
_;
}
/*///////////////////////////////////////////////////////////////
Constructor & Initializer logic
//////////////////////////////////////////////////////////////*/
/**
* See https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#initializing_the_implementation_contract
*
* @custom:oz-upgrades-unsafe-allow constructor
*/
constructor() {
_disableInitializers();
}
/**
* @dev Initializes the contract setting the `listingFee` fee and the `withdrawalPeriod`
* period. For more information about intializers, see
* https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#initializers.
*/
//slither-disable-next-line external-function
function initialize(uint256 listingFee_, uint256 withdrawalPeriod_) public initializer {
OwnableUpgradeable.__Ownable_init();
PausableUpgradeable.__Pausable_init();
ReentrancyGuardUpgradeable.__ReentrancyGuard_init();
PullPaymentUpgradeable.__PullPayment_init();
listingFee = listingFee_;
withdrawalPeriod = withdrawalPeriod_;
}
/*///////////////////////////////////////////////////////////////
Token listing logic (list, delist, buy, update)
//////////////////////////////////////////////////////////////*/
/**
* @dev Lists the NFT at the marketplace with the given `price`.
*
* Requirements:
*
* - The caller must be the owner of the NFT.
*
* - Before calling this method, the caller must approve thie contract to manage the NFT by
* calling the `approve` method on the corresponding NFT contract.
*
* - While calling this method, the caller must specify the value equal to the marketplace's
* listing fee, which can be retieved with the `listingFee` getter.
*
* - The listing `price` can be anything starting from 1 Wei.
*
* @param tokenContract The address of the NFT contract.
* @param tokenId The id of the NFT in the `tokenContract` contract.
* @param price The desired price to sell the NFT at the marketplace.
*
* Emits a {TokenListed} event.
*
* @custom:security non-reentrant
*/
function listToken(
IERC721 tokenContract,
uint256 tokenId,
uint256 price
)
external
payable
nonReentrant
isNFTOwner(tokenContract, tokenId, msg.sender)
isApproved(tokenContract, tokenId)
{
if (price == 0) {
revert ZeroPrice();
}
if (msg.value != listingFee) {
revert InvalidListingFee(tokenContract, tokenId, msg.value);
}
if (_tokenToListing[tokenContract][tokenId].price > 0) {
revert TokenAlreadyListed(tokenContract, tokenId);
}
_tokenToListing[tokenContract][tokenId] = Listing({ seller: msg.sender, price: price });
listingCount.increment();
address owner = super.owner();
paymentDates[owner] = block.timestamp;
super._asyncTransfer(owner, msg.value);
emit TokenListed(msg.sender, tokenContract, tokenId, price);
}
/**
* @dev Delists the NFT from the marketplace.
*
* Note that the information about the delisted NFT is wiped out completely from the
* marketplace contract.
*
* Requirements:
*
* - The NFT must be listed at the marketplace.
*
* - The caller must approve this contract to operate the NFT by calling the `approve` method
* on the corresponding NFT contract.
*
* - The caller must be the owner of the NFT.
*
* @param tokenContract The address of the NFT contract.
* @param tokenId The id of the NFT in the `tokenContract` contract.
*
* Emits a {TokenDelisted} event.
*
* @custom:security non-reentrant
*/
function delistToken(IERC721 tokenContract, uint256 tokenId)
external
isListed(tokenContract, tokenId)
nonReentrant
isNFTOwner(tokenContract, tokenId, msg.sender)
{
delete _tokenToListing[tokenContract][tokenId];
listingCount.decrement();
emit TokenDelisted(msg.sender, tokenContract, tokenId);
}
/**
* @dev Buys the NFT through the marketplace. The seller receives a payment from the buyer
* accumulated at the escrow and locked for the contract's withdrawal period.
*
* Note that the information about the bought NFT is wiped out completely from the marketplace
* contract.
*
* Requirements:
*
* - The NFT must be listed at the marketplace.
*
* - The caller must approve this contract to operate the NFT by calling the `approve` method
* on the corresponding NFT contract.
*
* - The caller must be the owner of the NFT.
*
* - The caller must transfer the value matching the NTF listed price.
*
* @param tokenContract The address of the NFT contract.
* @param tokenId The id of the NFT in the `tokenContract` contract.
*
* Emits a {TokenBought} event.
*
* @custom:security non-reentrant
*/
function buyToken(IERC721 tokenContract, uint256 tokenId)
external
payable
whenNotPaused
isListed(tokenContract, tokenId)
nonReentrant
isApproved(tokenContract, tokenId)
{
Listing memory listing = _tokenToListing[tokenContract][tokenId];
if (listing.price != msg.value) {
revert InvalidListingPrice(tokenContract, tokenId, msg.value);
}
if (tokenContract.ownerOf(tokenId) == msg.sender) {
revert PurchaseForbidden(msg.sender);
}
delete _tokenToListing[tokenContract][tokenId];
listingCount.decrement();
paymentDates[listing.seller] = block.timestamp + withdrawalPeriod;
super._asyncTransfer(listing.seller, msg.value);
tokenContract.safeTransferFrom(listing.seller, msg.sender, tokenId);
emit TokenBought(msg.sender, tokenContract, tokenId, listing.price);
}
/**
* @dev Updates the price of the listed NFT.
*
* Note the the caller is not prevented from setting the same price again.
*
* Requirements:
*
* - The NFT must be listed at the marketplace.
*
* - The caller must be the owner of the NFT.
*
* - The listing `newPrice` price can be anything starting from 1 Wei.
*
* @param tokenContract The address of the NFT contract.
* @param tokenId The id of the NFT in the `tokenContract` contract.
*
* Emits a {TokenListed} event.
*
* @custom:security non-reentrant
*/
function updateListing(
IERC721 tokenContract,
uint256 tokenId,
uint256 newPrice
)
external
isListed(tokenContract, tokenId)
nonReentrant
isNFTOwner(tokenContract, tokenId, msg.sender)
{
if (newPrice == 0) {
revert ZeroPrice();
}
_tokenToListing[tokenContract][tokenId].price = newPrice;
emit TokenListed(msg.sender, tokenContract, tokenId, newPrice);
}
/*///////////////////////////////////////////////////////////////
Payment withdrawal logic
//////////////////////////////////////////////////////////////*/
/**
* @dev Withdraws accumulated payments for the payee account.
*
* Requirements:
*
* - The caller must be either the `payee` or the owner account.
*
* - The withdrawal period must end up.
*
* @param payee The address of the `payee` account.
*
* Emits a {PaymentsWithdrawn} event.
*/
function withdrawPayments(address payable payee) public override whenNotPaused {
if (msg.sender != payee && msg.sender != super.owner()) {
revert WithdrawalForbidden(payee);
}
if (block.timestamp <= paymentDates[payee]) {
revert WithdrawalLocked(payee, block.timestamp, paymentDates[payee]);
}
delete paymentDates[payee];
uint256 amount = super.payments(payee);
super.withdrawPayments(payee);
emit PaymentsWithdrawn(payee, amount);
}
/*///////////////////////////////////////////////////////////////
Contract administration logic
//////////////////////////////////////////////////////////////*/
/**
* @dev Sets the listing fee of the marketplace. The fee is expressed in Wei and can be
* anything starting from zero.
*
* It is an adminstrative method that can be called by the owner only.
*
* Note the caller is not prevented from setting the same listing fee again.
*
* @param listingFee_ The listing fee of the marketplace.
*
* Emits a {ListingFeeSet} event.
*/
function setListingFee(uint256 listingFee_) external onlyOwner {
listingFee = listingFee_;
emit ListingFeeSet(listingFee_);
}
/**
* @dev Sets the withdrawal period for pending payments. The period is expressed in seconds and
* can be anything starting from zero.
*
* It is an adminstrative method that can be called by the owner only.
*
* Note the caller is not prevented from setting the same withdrawal period again.
*
* @param withdrawalPeriod_ The withdrawal period for pending payments (in seconds).
*
* Emits a {WithdrawalPeriodSet} event.
*/
function setWithdrawalPeriod(uint256 withdrawalPeriod_) external onlyOwner {
withdrawalPeriod = withdrawalPeriod_;
emit WithdrawalPeriodSet(withdrawalPeriod_);
}
/**
* @dev Performs an emergency stop on the contract for the `buyToken` and `withdrawPayments`
* methods.
*
* It is an adminstrative method that can be called by the owner only.
*/
function pause() external onlyOwner {
super._pause();
}
/**
* @dev Releases an emergency stop on the contract for the `buyToken` and `withdrawPayments`
* methods.
*
* It is an adminstrative method that can be called by the owner only.
*/
function unpause() external onlyOwner {
super._unpause();
}
/*///////////////////////////////////////////////////////////////
Getters
//////////////////////////////////////////////////////////////*/
/**
* @dev Returns the information about the listed NFT, if any.
*/
function getListing(IERC721 tokenContract, uint256 tokenId)
external
view
returns (Listing memory)
{
return _tokenToListing[tokenContract][tokenId];
}
}