/
SingleRandomWinner.sol
459 lines (390 loc) · 19 KB
/
SingleRandomWinner.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
pragma solidity 0.6.4;
import "@openzeppelin/contracts-ethereum-package/contracts/access/Ownable.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/utils/SafeCast.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/introspection/IERC1820Registry.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/utils/ReentrancyGuard.sol";
import "@pooltogether/fixed-point/contracts/FixedPoint.sol";
import "./SingleRandomWinnerStorage.sol";
import "../../token/TokenControllerInterface.sol";
import "../../token/ControlledToken.sol";
import "../../prize-pool/PrizePool.sol";
import "../../Constants.sol";
import "../../utils/RelayRecipient.sol";
/* solium-disable security/no-block-members */
contract SingleRandomWinner is SingleRandomWinnerStorage,
Initializable,
OwnableUpgradeSafe,
RelayRecipient,
ReentrancyGuardUpgradeSafe,
PrizePoolTokenListenerInterface {
using SafeMath for uint256;
using SafeCast for uint256;
using MappedSinglyLinkedList for MappedSinglyLinkedList.Mapping;
uint256 internal constant ETHEREUM_BLOCK_TIME_ESTIMATE_MANTISSA = 13.4 ether;
event PrizePoolOpened(
address indexed operator,
uint256 indexed prizePeriodStartedAt
);
event PrizePoolAwardStarted(
address indexed operator,
address indexed prizePool,
uint32 indexed rngRequestId,
uint32 rngLockBlock
);
event PrizePoolAwarded(
address indexed operator,
uint256 randomNumber,
uint256 prize
);
event RngServiceUpdated(
address rngService
);
event ExternalErc721AwardAdded(
address indexed externalErc721,
uint256[] tokenIds
);
event ExternalErc20AwardAdded(
address indexed externalErc20
);
event ExternalErc721AwardRemoved(
address indexed externalErc721Award
);
event ExternalErc20AwardRemoved(
address indexed externalErc20Award
);
function initialize (
address _trustedForwarder,
uint256 _prizePeriodStart,
uint256 _prizePeriodSeconds,
PrizePool _prizePool,
address _ticket,
address _sponsorship,
RNGInterface _rng,
address[] memory _externalErc20s
) public initializer {
require(_prizePeriodSeconds > 0, "SingleRandomWinner/prize-period-greater-than-zero");
require(address(_prizePool) != address(0), "SingleRandomWinner/prize-pool-not-zero");
require(address(_ticket) != address(0), "SingleRandomWinner/ticket-not-zero");
require(address(_sponsorship) != address(0), "SingleRandomWinner/sponsorship-not-zero");
require(address(_rng) != address(0), "SingleRandomWinner/rng-not-zero");
prizePool = _prizePool;
ticket = TicketInterface(_ticket);
rng = _rng;
sponsorship = IERC20(_sponsorship);
trustedForwarder = _trustedForwarder;
__Ownable_init();
__ReentrancyGuard_init();
Constants.REGISTRY.setInterfaceImplementer(address(this), Constants.TOKENS_RECIPIENT_INTERFACE_HASH, address(this));
for (uint256 i = 0; i < _externalErc20s.length; i++) {
require(prizePool.canAwardExternal(_externalErc20s[i]), "SingleRandomWinner/cannot-award-external");
}
externalErc20s.initialize();
externalErc20s.addAddresses(_externalErc20s);
prizePeriodSeconds = _prizePeriodSeconds;
prizePeriodStartedAt = _prizePeriodStart;
externalErc721s.initialize();
emit PrizePoolOpened(_msgSender(), prizePeriodStartedAt);
}
/// @notice Calculates and returns the currently accrued prize
/// @return The current prize size
function currentPrize() public view returns (uint256) {
return prizePool.awardBalance();
}
/// @notice Estimates the prize size using the default ETHEREUM_BLOCK_TIME_ESTIMATE_MANTISSA
/// @return The estimated final size of the prize
function estimatePrize() public view returns (uint256) {
return estimatePrizeWithBlockTime(ETHEREUM_BLOCK_TIME_ESTIMATE_MANTISSA);
}
/// @notice Estimates the prize size given the passed number of seconds per block
/// @param secondsPerBlockMantissa The seconds per block to use for the calculation. Should be a fixed point 18 number like Ether.
/// @return The estimated final size of the prize.
function estimatePrizeWithBlockTime(uint256 secondsPerBlockMantissa) public view returns (uint256) {
return currentPrize().add(estimateRemainingPrizeWithBlockTime(secondsPerBlockMantissa));
}
/// @notice Estimates the size of the *remaining* prize to accrue.
/// This function uses the constant ETHEREUM_BLOCK_TIME_ESTIMATE_MANTISSA to calculate the accrued interest.
/// @return The estimated remaining prize
function estimateRemainingPrize() public view returns (uint256) {
return estimateRemainingPrizeWithBlockTime(ETHEREUM_BLOCK_TIME_ESTIMATE_MANTISSA);
}
/// @notice Estimates the size of the *remaining* prize to accrue. Allows the user to pass the seconds per block value.
/// @param secondsPerBlockMantissa The seconds per block to use for the calculation. Should be a fixed point 18 number like Ether.
/// @return The estimated remaining prize
function estimateRemainingPrizeWithBlockTime(uint256 secondsPerBlockMantissa) public view returns (uint256) {
uint256 remaining = prizePool.estimateAccruedInterestOverBlocks(
prizePool.accountedBalance(),
estimateRemainingBlocksToPrize(secondsPerBlockMantissa)
);
uint256 reserveFee = prizePool.calculateReserveFee(remaining);
return remaining.sub(reserveFee);
}
/// @notice Estimates the remaining blocks until the prize given a number of seconds per block
/// @param secondsPerBlockMantissa The number of seconds per block to use for the calculation. Should be a fixed point 18 number like Ether.
/// @return The estimated number of blocks remaining until the prize can be awarded.
function estimateRemainingBlocksToPrize(uint256 secondsPerBlockMantissa) public view returns (uint256) {
return FixedPoint.divideUintByMantissa(
_prizePeriodRemainingSeconds(),
secondsPerBlockMantissa
);
}
/// @notice Returns the number of seconds remaining until the prize can be awarded.
/// @return The number of seconds remaining until the prize can be awarded.
function prizePeriodRemainingSeconds() external view returns (uint256) {
return _prizePeriodRemainingSeconds();
}
/// @notice Returns the number of seconds remaining until the prize can be awarded.
/// @return The number of seconds remaining until the prize can be awarded.
function _prizePeriodRemainingSeconds() internal view returns (uint256) {
uint256 endAt = _prizePeriodEndAt();
uint256 time = _currentTime();
if (time > endAt) {
return 0;
}
return endAt.sub(time);
}
/// @notice Returns whether the prize period is over
/// @return True if the prize period is over, false otherwise
function isPrizePeriodOver() external view returns (bool) {
return _isPrizePeriodOver();
}
/// @notice Returns whether the prize period is over
/// @return True if the prize period is over, false otherwise
function _isPrizePeriodOver() internal view returns (bool) {
return _currentTime() >= _prizePeriodEndAt();
}
/// @notice Awards collateral as tickets to a user
/// @param user The user to whom the tickets are minted
/// @param amount The amount of interest to mint as tickets.
function _awardTickets(address user, uint256 amount) internal {
prizePool.award(user, amount, address(ticket));
}
/// @notice Awards all external tokens with non-zero balances to the given user. The external tokens must be held by the PrizePool contract.
/// @param winner The user to transfer the tokens to
function _awardAllExternalTokens(address winner) internal {
_awardExternalErc20s(winner);
_awardExternalErc721s(winner);
}
/// @notice Awards all external ERC20 tokens with non-zero balances to the given user.
/// The external tokens must be held by the PrizePool contract.
/// @param winner The user to transfer the tokens to
function _awardExternalErc20s(address winner) internal {
address currentToken = externalErc20s.start();
while (currentToken != address(0) && currentToken != externalErc20s.end()) {
uint256 balance = IERC20(currentToken).balanceOf(address(prizePool));
if (balance > 0) {
prizePool.awardExternalERC20(winner, currentToken, balance);
}
currentToken = externalErc20s.next(currentToken);
}
}
/// @notice Awards all external ERC721 tokens to the given user.
/// The external tokens must be held by the PrizePool contract.
/// @dev The list of ERC721s is reset after every award
/// @param winner The user to transfer the tokens to
function _awardExternalErc721s(address winner) internal {
address currentToken = externalErc721s.start();
while (currentToken != address(0) && currentToken != externalErc721s.end()) {
uint256 balance = IERC721(currentToken).balanceOf(address(prizePool));
if (balance > 0) {
prizePool.awardExternalERC721(winner, currentToken, externalErc721TokenIds[currentToken]);
delete externalErc721TokenIds[currentToken];
}
currentToken = externalErc721s.next(currentToken);
}
externalErc721s.clearAll();
}
/// @notice Returns the timestamp at which the prize period ends
/// @return The timestamp at which the prize period ends.
function prizePeriodEndAt() external view returns (uint256) {
// current prize started at is non-inclusive, so add one
return _prizePeriodEndAt();
}
/// @notice Returns the timestamp at which the prize period ends
/// @return The timestamp at which the prize period ends.
function _prizePeriodEndAt() internal view returns (uint256) {
// current prize started at is non-inclusive, so add one
return prizePeriodStartedAt.add(prizePeriodSeconds);
}
/// @notice Called by the PrizePool for transfers of controlled tokens
/// @dev Note that this is only for *transfers*, not mints or burns
/// @param from The user whose tokens are being transferred
/// @param to The user who is receiving the tokens.
/// @param amount The amount of tokens being sent.
/// @param controlledToken The type of collateral that is being sent
function beforeTokenTransfer(address from, address to, uint256 amount, address controlledToken) external override onlyPrizePool {
if (controlledToken == address(ticket)) {
_requireNotLocked();
}
}
/// @notice Called by the PrizePool when minting controlled tokens
/// @param to The user who is receiving the tokens.
/// @param amount The amount of tokens being minted.
/// @param controlledToken The type of collateral that is being minted
/// @param referrer The address that referred the mint
function beforeTokenMint(
address to,
uint256 amount,
address controlledToken,
address referrer
)
external
override
onlyPrizePool
{
if (controlledToken == address(ticket)) {
_requireNotLocked();
}
}
/// @notice returns the current time. Used for testing.
/// @return The current time (block.timestamp)
function _currentTime() internal virtual view returns (uint256) {
return block.timestamp;
}
/// @notice returns the current time. Used for testing.
/// @return The current time (block.timestamp)
function _currentBlock() internal virtual view returns (uint256) {
return block.number;
}
/// @notice Starts the award process by starting random number request. The prize period must have ended.
/// @dev The RNG-Request-Fee is expected to be held within this contract before calling this function
function startAward() external requireCanStartAward {
(address feeToken, uint256 requestFee) = rng.getRequestFee();
if (feeToken != address(0) && requestFee > 0) {
IERC20(feeToken).approve(address(rng), requestFee);
}
(uint32 requestId, uint32 lockBlock) = rng.requestRandomNumber();
rngRequest.id = requestId;
rngRequest.lockBlock = lockBlock;
emit PrizePoolAwardStarted(_msgSender(), address(prizePool), requestId, lockBlock);
}
/// @notice Completes the award process and awards the winners. The random number must have been requested and is now available.
function completeAward() external requireCanCompleteAward {
uint256 randomNumber = rng.randomNumber(rngRequest.id);
delete rngRequest;
uint256 prize = prizePool.captureAwardBalance();
address winner = ticket.draw(randomNumber);
if (winner != address(0)) {
_awardTickets(winner, prize);
_awardAllExternalTokens(winner);
}
// to avoid clock drift, we should calculate the start time based on the previous period start time.
prizePeriodStartedAt = _calculateNextPrizePeriodStartTime(_currentTime());
emit PrizePoolAwarded(_msgSender(), randomNumber, prize);
emit PrizePoolOpened(_msgSender(), prizePeriodStartedAt);
}
function _calculateNextPrizePeriodStartTime(uint256 currentTime) internal view returns (uint256) {
uint256 elapsedPeriods = currentTime.sub(prizePeriodStartedAt).div(prizePeriodSeconds);
return prizePeriodStartedAt.add(elapsedPeriods.mul(prizePeriodSeconds));
}
function calculateNextPrizePeriodStartTime(uint256 currentTime) external view returns (uint256) {
return _calculateNextPrizePeriodStartTime(currentTime);
}
/// @notice Returns whether an award process can be started
/// @return True if an award can be started, false otherwise.
function canStartAward() external view returns (bool) {
return _isPrizePeriodOver() && !isRngRequested();
}
/// @notice Returns whether an award process can be completed
/// @return True if an award can be completed, false otherwise.
function canCompleteAward() external view returns (bool) {
return isRngRequested() && isRngCompleted();
}
/// @notice Returns whether a random number has been requested
/// @return True if a random number has been requested, false otherwise.
function isRngRequested() public view returns (bool) {
return rngRequest.id != 0;
}
/// @notice Returns whether the random number request has completed.
/// @return True if a random number request has completed, false otherwise.
function isRngCompleted() public view returns (bool) {
return rng.isRequestComplete(rngRequest.id);
}
/// @notice Returns the block number that the current RNG request has been locked to
/// @return The block number that the RNG request is locked to
function getLastRngLockBlock() external view returns (uint32) {
return rngRequest.lockBlock;
}
/// @notice Returns the current RNG Request ID
/// @return The current Request ID
function getLastRngRequestId() external view returns (uint32) {
return rngRequest.id;
}
/// @notice Sets the RNG service that the Prize Strategy is connected to
/// @param rngService The address of the new RNG service interface
function setRngService(RNGInterface rngService) external onlyOwner {
require(!isRngRequested(), "SingleRandomWinner/rng-in-flight");
rng = rngService;
emit RngServiceUpdated(address(rngService));
}
/// @notice Adds an external ERC20 token type as an additional prize that can be awarded
/// @dev Only the Prize-Strategy owner/creator can assign external tokens,
/// and they must be approved by the Prize-Pool
/// @param _externalErc20 The address of an ERC20 token to be awarded
function addExternalErc20Award(address _externalErc20) external onlyOwner {
require(prizePool.canAwardExternal(_externalErc20), "SingleRandomWinner/cannot-award-external");
externalErc20s.addAddress(_externalErc20);
emit ExternalErc20AwardAdded(_externalErc20);
}
/// @notice Removes an external ERC20 token type as an additional prize that can be awarded
/// @dev Only the Prize-Strategy owner/creator can remove external tokens
/// @param _externalErc20 The address of an ERC20 token to be removed
/// @param _prevExternalErc20 The address of the previous ERC20 token in the `externalErc20s` list.
/// If the ERC20 is the first address, then the previous address is the SENTINEL address: 0x0000000000000000000000000000000000000001
function removeExternalErc20Award(address _externalErc20, address _prevExternalErc20) external onlyOwner {
externalErc20s.removeAddress(_prevExternalErc20, _externalErc20);
emit ExternalErc20AwardRemoved(_externalErc20);
}
/// @notice Adds an external ERC721 token as an additional prize that can be awarded
/// @dev Only the Prize-Strategy owner/creator can assign external tokens,
/// and they must be approved by the Prize-Pool
/// NOTE: The NFT must already be owned by the Prize-Pool
/// @param _externalErc721 The address of an ERC721 token to be awarded
/// @param _tokenIds An array of token IDs of the ERC721 to be awarded
function addExternalErc721Award(address _externalErc721, uint256[] calldata _tokenIds) external onlyOwner {
require(prizePool.canAwardExternal(_externalErc721), "SingleRandomWinner/cannot-award-external");
externalErc721s.addAddress(_externalErc721);
for (uint256 i = 0; i < _tokenIds.length; i++) {
uint256 tokenId = _tokenIds[i];
require(IERC721(_externalErc721).ownerOf(tokenId) == address(prizePool), "SingleRandomWinner/unavailable-token");
externalErc721TokenIds[_externalErc721].push(tokenId);
}
emit ExternalErc721AwardAdded(_externalErc721, _tokenIds);
}
/// @notice Removes an external ERC721 token as an additional prize that can be awarded
/// @dev Only the Prize-Strategy owner/creator can remove external tokens
/// @param _externalErc721 The address of an ERC721 token to be removed
/// @param _prevExternalErc721 The address of the previous ERC721 token in the list.
/// If no previous, then pass the SENTINEL address: 0x0000000000000000000000000000000000000001
function removeExternalErc721Award(address _externalErc721, address _prevExternalErc721) external onlyOwner {
externalErc721s.removeAddress(_prevExternalErc721, _externalErc721);
delete externalErc721TokenIds[_externalErc721];
emit ExternalErc721AwardRemoved(_externalErc721);
}
function _requireNotLocked() internal view {
require(rngRequest.lockBlock == 0 || _currentBlock() < rngRequest.lockBlock, "SingleRandomWinner/rng-in-flight");
}
modifier requireNotLocked() {
_requireNotLocked();
_;
}
modifier requireCanStartAward() {
require(_isPrizePeriodOver(), "SingleRandomWinner/prize-period-not-over");
require(!isRngRequested(), "SingleRandomWinner/rng-already-requested");
_;
}
modifier requireCanCompleteAward() {
require(_isPrizePeriodOver(), "SingleRandomWinner/prize-period-not-over");
require(isRngRequested(), "SingleRandomWinner/rng-not-requested");
require(isRngCompleted(), "SingleRandomWinner/rng-not-complete");
_;
}
modifier onlyPrizePool() {
require(_msgSender() == address(prizePool), "SingleRandomWinner/only-prize-pool");
_;
}
function _msgSender() internal override(BaseRelayRecipient, ContextUpgradeSafe) virtual view returns (address payable) {
return BaseRelayRecipient._msgSender();
}
}