This repository has been archived by the owner on Aug 12, 2023. It is now read-only.
generated from refcell/femplate
-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathCloak.sol
528 lines (395 loc) · 19.2 KB
/
Cloak.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: AGPL-3.0-only
pragma solidity >=0.8.0;
import {IERC20} from "./interfaces/IERC20.sol";
import {IERC721TokenReceiver} from "./interfaces/IERC721TokenReceiver.sol";
import {FixedPointMathLib} from "@solmate/utils/FixedPointMathLib.sol";
/// ⠄⠄⠄⠄⠄⠄⠄⠄⢀⣀⣤⣴⣶⠞⠛⢶⣤⣄⡀⠄⠄⠄⠄⠄⠄⠄⠄⠄ ///
/// ⠄⠄⠄⠄⠄⣠⡶⠿⠿⠿⠿⠟⠁⣰⠇⣈⠻⣿⣿⣷⣶⣤⣀⠄⠄⠄⠄⠄ ///
/// ⠄⠄⠄⢠⣾⣿⡗⢿⣶⣶⣤⣴⣾⠟⢠⡏⠄⠄⠈⠙⠿⣿⣿⣷⣦⠄⠄⠄ ///
/// ⠄⠄⢠⣿⣿⣿⠇⢶⣤⣀⡺⠿⠋⣠⡾⠄⠄⢤⣶⣤⣄⠈⠛⢿⣿⣷⡄⠄ ///
/// ⠄⢀⣿⣿⣿⣣⣿⣷⣌⡛⢿⣿⣾⡟⠁⢤⣤⣀⠙⢿⣿⣷⣄⠄⠙⢿⣷⠄ ///
/// ⠄⣼⣿⣿⣳⣿⣿⣿⣿⣿⣷⣦⢭⣶⣇⠄⠻⣿⣧⡀⠙⢿⣿⣷⣦⡀⠙⠇ ///
/// ⢰⣿⣿⣳⣿⣿⣻⣿⢿⣿⣿⣿⣿⣿⣿⣷⡀⠹⣿⣿⣄⠄⠹⣿⣿⣴⡄⠄ ///
/// ⢸⡿⣱⣿⣿⣏⣿⣿⢸⣿⣿⣧⣿⣿⣿⣿⣷⡀⠘⣿⣿⣦⠄⠈⢿⡿⣱⣿ ///
/// ⠘⣵⣿⣿⣿⣸⣿⣿⢾⣿⣿⣿⢸⣿⣿⣿⣿⣷⠄⡜⣿⣿⣷⠄⠄⠁⣿⡿ ///
/// ⢸⣶⣍⢿⢧⣿⣿⣿⢸⣿⣿⣿⢸⣿⣿⣿⣿⣿⣇⠘⡜⣿⣷⣴⣦⣀⠘⠄ ///
/// ⠄⠻⣿⢇⣾⣿⣿⣿⢸⣿⣿⣿⡯⣿⣿⣿⣿⣿⣿⡆⠘⡽⡟⢫⣴⣶⡆⠄ ///
/// ⠄⠄⠙⢷⣿⡭⠡⠆⢸⣿⣿⣿⡇⠿⣿⣿⣿⣿⠛⠻⠄⢫⠄⣀⡹⣿⡇⠄ ///
/// ⠄⠄⠄⠄⠙⠃⠄⢀⣚⣭⣭⣭⡍⠄⣿⣿⣿⡿⢟⣛⣂⠄⣼⡿⣣⡟⠄⠄ ///
/// ⠄⠄⠄⠄⠄⠄⠉⠙⠻⣿⣿⣿⣁⣀⣈⣩⣭⣶⣿⣿⣿⣷⣭⡶⠋⠄⠄⠄ ///
/// @title Cloak
/// @author andreas <andreas@nascent.xyz>
/// @dev Extensible ERC721 Implementation with a Built-in Commit-Reveal Scheme.
abstract contract Cloak {
///////////////////////////////////////////////////////////////////////////////
/// CUSTOM ERRORS ///
///////////////////////////////////////////////////////////////////////////////
error NotAuthorized();
error WrongFrom();
error InvalidRecipient();
error UnsafeRecipient();
error AlreadyMinted();
error NotMinted();
error InsufficientDeposit();
error WrongPhase();
error InvalidHash();
error InsufficientPrice();
error InsufficientValue();
error InvalidAction();
///////////////////////////////////////////////////////////////////////////////
/// EVENTS ///
///////////////////////////////////////////////////////////////////////////////
event Commit(address indexed from);
event Reveal(address indexed from, uint256 appraisal);
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed spender, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
///////////////////////////////////////////////////////////////////////////////
/// METADATA ///
///////////////////////////////////////////////////////////////////////////////
string public name;
string public symbol;
function tokenURI(uint256 id) public view virtual returns (string memory);
///////////////////////////////////////////////////////////////////////////////
/// IMMUTABLES ///
///////////////////////////////////////////////////////////////////////////////
/// @dev The deposit amount to place a commitment
uint256 public immutable depositAmount;
/// @dev The minimum mint price
/// @dev Commit Start Timestamp
uint256 public immutable commitStart;
/// @dev Reveal Start Timestamp
uint256 public immutable revealStart;
/// @dev Mint Start Timestamp
uint256 public immutable mintStart;
/// @dev Optional ERC20 Deposit Token
address public immutable depositToken;
/// @dev Flex is a scaling factor for standard deviation in price band calculation
uint256 public immutable flex;
///////////////////////////////////////////////////////////////////////////////
/// CUSTOM STORAGE ///
///////////////////////////////////////////////////////////////////////////////
uint256 public minPrice;
/// @dev The outlier scale for loss penalty
/// @dev Loss penalty is taken with OUTLIER_FLEX * error as a percent
uint256 public constant OUTLIER_FLEX = 5;
/// @dev A rolling variance calculation
/// @dev Used for minting price bands
uint256 public rollingVariance;
/// @dev The number of commits calculated
uint256 public count;
/// @dev The result cumulative sum
uint256 public resultPrice;
/// @dev The total token supply
uint256 public totalSupply;
/// @dev User Commitments
mapping(address => bytes32) public commits;
/// @dev The resulting user appraisals
mapping(address => uint256) public reveals;
///////////////////////////////////////////////////////////////////////////////
/// ERC721 STORAGE ///
///////////////////////////////////////////////////////////////////////////////
mapping(address => uint256) public balanceOf;
mapping(uint256 => address) public ownerOf;
mapping(uint256 => address) public getApproved;
mapping(address => mapping(address => bool)) public isApprovedForAll;
///////////////////////////////////////////////////////////////////////////////
/// CONSTRUCTOR ///
///////////////////////////////////////////////////////////////////////////////
constructor(
string memory _name,
string memory _symbol,
uint256 _depositAmount,
bytes32 _minPrice,
uint256 _commitStart,
uint256 _revealStart,
uint256 _mintStart,
address _depositToken,
uint256 _flex
) {
name = _name;
symbol = _symbol;
// Store immutables
depositAmount = _depositAmount;
commitStart = _commitStart;
revealStart = _revealStart;
mintStart = _mintStart;
depositToken = _depositToken;
flex = _flex;
// Set for price reveal (avoid storing deployer address)
minPrice = 0;
// WARNING: Stops deployer of a contract from commiting anything until post-reveal phase
commits[msg.sender] = _minPrice;
}
///////////////////////////////////////////////////////////////////////////////
/// COMMITMENT LOGIC ///
///////////////////////////////////////////////////////////////////////////////
/// @notice Commit is payable to require the deposit amount
function commit(bytes32 commitment) external payable {
// Make sure the user has placed the deposit amount
if (depositToken == address(0) && msg.value < depositAmount) revert InsufficientDeposit();
// Verify during commit phase
if (block.timestamp < commitStart || block.timestamp >= revealStart) revert WrongPhase();
// Transfer the deposit token into this contract
if (depositToken != address(0)) {
IERC20(depositToken).transferFrom(msg.sender, address(this), depositAmount);
}
// Update a user's commitment if one's outstanding
// if (commits[msg.sender] != bytes32(0)) count += 1;
commits[msg.sender] = commitment;
// Emit the commit event
emit Commit(msg.sender);
}
/// @notice Revealing a commitment
function reveal(uint256 appraisal, bytes32 blindingFactor) external {
// Verify during reveal+mint phase
if (block.timestamp < revealStart || block.timestamp >= mintStart) revert WrongPhase();
bytes32 senderCommit = commits[msg.sender];
bytes32 calculatedCommit = keccak256(abi.encodePacked(msg.sender, appraisal, blindingFactor));
if (senderCommit != calculatedCommit) revert InvalidHash();
// The user has revealed their correct value
delete commits[msg.sender];
reveals[msg.sender] = appraisal;
// Add the appraisal to the result value and recalculate variance
// Calculation adapted from https://math.stackexchange.com/questions/102978/incremental-computation-of-standard-deviation
if (count == 0) {
resultPrice = appraisal;
} else {
uint256 resultPrice_ = resultPrice;
uint256 newResultPrice = (count * resultPrice_ + appraisal) / (count + 1);
uint256 carryTerm = count * rollingVariance;
uint256 clearingDiff = resultPrice_ > newResultPrice ? resultPrice_ - newResultPrice : newResultPrice - resultPrice_;
uint256 deviationUpdate = count * (clearingDiff ** 2);
uint256 meanUpdate = appraisal < newResultPrice ? newResultPrice - appraisal : appraisal - newResultPrice;
uint256 updateTerm = meanUpdate ** 2;
rollingVariance = (deviationUpdate + carryTerm + updateTerm) / (count + 1);
// Update resultPrice (new mean)
resultPrice = newResultPrice;
}
unchecked {
count += 1;
}
// Emit a Reveal Event
emit Reveal(msg.sender, appraisal);
}
///////////////////////////////////////////////////////////////////////////////
/// MINT LOGIC ///
///////////////////////////////////////////////////////////////////////////////
/// @notice Enables Minting During the Minting Phase
function mint() external payable {
// Verify during mint phase
if (block.timestamp < mintStart) revert WrongPhase();
// If deployer forgets to reveal before mint, set to variant pricing
if (minPrice == 0) minPrice = resultPrice;
// Sload the user's appraisal value
uint256 senderAppraisal = reveals[msg.sender];
// Result value
uint256 _resultPrice = resultPrice;
uint256 finalValue = _resultPrice;
if (_resultPrice < minPrice) finalValue = minPrice;
// Verify they sent at least enough to cover the mint cost
if (depositToken == address(0) && msg.value < finalValue) revert InsufficientValue();
if (depositToken != address(0)) IERC20(depositToken).transferFrom(msg.sender, address(this), finalValue);
// Use Reveals as a mask
if (reveals[msg.sender] == 0) revert InvalidAction();
// Check that the appraisal is within the price band
uint256 stdDev = FixedPointMathLib.sqrt(rollingVariance);
if (senderAppraisal < (_resultPrice - flex * stdDev) || senderAppraisal > (_resultPrice + flex * stdDev)) {
revert InsufficientPrice();
}
// Delete revealed value to prevent double spend
delete reveals[msg.sender];
// Send deposit back to the minter
if(depositToken == address(0)) msg.sender.call{value: depositAmount}("");
else IERC20(depositToken).transfer(msg.sender, depositAmount);
// Otherwise, we can mint the token
unchecked {
_mint(msg.sender, totalSupply++);
}
}
/// @notice Forgos a mint
/// @notice A penalty is assumed if the user's sealed bid was within the minting threshold
function forgo() external {
// Verify during mint phase
if (block.timestamp < mintStart) revert WrongPhase();
// Use Reveals as a mask
if (reveals[msg.sender] == 0) revert InvalidAction();
uint256 _resultPrice = resultPrice;
// Sload the user's appraisal value
uint256 senderAppraisal = reveals[msg.sender];
// Calculate a Loss penalty
uint256 lossPenalty = 0;
uint256 stdDev = FixedPointMathLib.sqrt(rollingVariance);
uint256 diff = senderAppraisal < _resultPrice ? _resultPrice - senderAppraisal : senderAppraisal - _resultPrice;
if (stdDev != 0 && senderAppraisal >= (_resultPrice - flex * stdDev) && senderAppraisal <= (_resultPrice + flex * stdDev)) {
lossPenalty = ((diff / stdDev) * depositAmount) / 100;
}
// Increase loss penalty if it's an outlier using Z-scores
if (stdDev != 0) {
// Take a penalty of OUTLIER_FLEX * error as a percent
lossPenalty += OUTLIER_FLEX * (diff / stdDev) * depositAmount / 100;
}
// Return the deposit less the loss penalty
// NOTE: we can let this error on underflow since that means Cloak should keep the full deposit
uint256 amountTransfer = depositAmount - lossPenalty;
// Transfer eth or erc20 back to user
delete reveals[msg.sender];
if(depositToken == address(0)) msg.sender.call{value: amountTransfer}("");
else IERC20(depositToken).transfer(msg.sender, amountTransfer);
}
/// @notice Allows a user to withdraw their deposit on reveal elusion
function lostReveal() external {
// Verify after the reveal phase
if (block.timestamp < mintStart) revert WrongPhase();
// Prevent withdrawals unless reveals is empty and commits isn't
if (reveals[msg.sender] != 0 || commits[msg.sender] == 0) revert InvalidAction();
// Prevent double spends
delete commits[msg.sender];
// Then we can release deposit
uint256 lossyDeposit = depositAmount;
// This shan't underflow
unchecked {
lossyDeposit = lossyDeposit - ((lossyDeposit * 5_000) / 10_000);
}
// Transfer back
if(depositToken == address(0)) msg.sender.call{value: lossyDeposit}("");
else IERC20(depositToken).transfer(msg.sender, lossyDeposit);
}
/// @notice Allows a user to view if they can mint
function canMint() external view returns (bool mintable) {
// Sload the user's appraisal value
uint256 senderAppraisal = reveals[msg.sender];
uint256 stdDev = FixedPointMathLib.sqrt(rollingVariance);
mintable = senderAppraisal >= (resultPrice - flex * stdDev) && senderAppraisal <= (resultPrice + flex * stdDev);
}
/// @notice Deployer reveals price only before minting
function priceReveal(uint256 price, bytes32 blindingFactor) external {
if (block.timestamp < revealStart || block.timestamp >= mintStart) revert WrongPhase();
bytes32 calculatedCommit = keccak256(abi.encodePacked(msg.sender, price, blindingFactor));
if (commits[msg.sender] != calculatedCommit) revert NotAuthorized();
minPrice = price;
}
///////////////////////////////////////////////////////////////////////////////
/// ERC721 LOGIC ///
///////////////////////////////////////////////////////////////////////////////
function approve(address spender, uint256 id) public virtual {
address owner = ownerOf[id];
if (msg.sender != owner && !isApprovedForAll[owner][msg.sender]) {
revert NotAuthorized();
}
getApproved[id] = spender;
emit Approval(owner, spender, id);
}
function setApprovalForAll(address operator, bool approved) public virtual {
isApprovedForAll[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
function transferFrom(
address from,
address to,
uint256 id
) public virtual {
if (from != ownerOf[id]) revert WrongFrom();
if (to == address(0)) revert InvalidRecipient();
if (msg.sender != from && msg.sender != getApproved[id] && !isApprovedForAll[from][msg.sender]) {
revert NotAuthorized();
}
// Underflow impossible due to check for ownership
unchecked {
balanceOf[from]--;
balanceOf[to]++;
}
ownerOf[id] = to;
delete getApproved[id];
emit Transfer(from, to, id);
}
function safeTransferFrom(
address from,
address to,
uint256 id
) public virtual {
transferFrom(from, to, id);
if (
to.code.length != 0 &&
IERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") !=
IERC721TokenReceiver.onERC721Received.selector
) {
revert UnsafeRecipient();
}
}
function safeTransferFrom(
address from,
address to,
uint256 id,
bytes memory data
) public virtual {
transferFrom(from, to, id);
if (
to.code.length != 0 &&
IERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) !=
IERC721TokenReceiver.onERC721Received.selector
) {
revert UnsafeRecipient();
}
}
///////////////////////////////////////////////////////////////////////////////
/// ERC165 LOGIC ///
///////////////////////////////////////////////////////////////////////////////
function supportsInterface(bytes4 interfaceId) public pure virtual returns (bool) {
return
interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165
interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721
interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata
}
///////////////////////////////////////////////////////////////////////////////
/// INTERNAL LOGIC ///
///////////////////////////////////////////////////////////////////////////////
function _mint(address to, uint256 id) internal virtual {
if (to == address(0)) revert InvalidRecipient();
if (ownerOf[id] != address(0)) revert AlreadyMinted();
// Counter overflow is incredibly unrealistic.
unchecked {
balanceOf[to]++;
}
ownerOf[id] = to;
emit Transfer(address(0), to, id);
}
function _burn(uint256 id) internal virtual {
address owner = ownerOf[id];
if (ownerOf[id] == address(0)) revert NotMinted();
// Ownership check above ensures no underflow.
unchecked {
balanceOf[owner]--;
}
delete ownerOf[id];
delete getApproved[id];
emit Transfer(owner, address(0), id);
}
///////////////////////////////////////////////////////////////////////////////
/// INTERNAL SAFE LOGIC ///
///////////////////////////////////////////////////////////////////////////////
function _safeMint(address to, uint256 id) internal virtual {
_mint(to, id);
if (
to.code.length != 0 &&
IERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, "") !=
IERC721TokenReceiver.onERC721Received.selector
) {
revert UnsafeRecipient();
}
}
function _safeMint(
address to,
uint256 id,
bytes memory data
) internal virtual {
_mint(to, id);
if (
to.code.length != 0 &&
IERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, data) !=
IERC721TokenReceiver.onERC721Received.selector
) {
revert UnsafeRecipient();
}
}
}