-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathProcessingPlant.sol
312 lines (250 loc) · 11 KB
/
ProcessingPlant.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
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import {IridiumToken} from "./IridiumToken.sol";
import {Geode} from "./Geode.sol";
import {AccessControl} from "openzeppelin-contracts/access/AccessControl.sol";
import {ReentrancyGuard} from "openzeppelin-contracts/security/ReentrancyGuard.sol";
import {ERC1155Holder} from "openzeppelin-contracts/token/ERC1155/utils/ERC1155Holder.sol";
import {ERC1155Receiver} from "openzeppelin-contracts/token/ERC1155/utils/ERC1155Receiver.sol";
import {VRFConsumerBaseV2} from "chainlink-contracts/VRFConsumerBaseV2.sol";
import {VRFCoordinatorV2Interface} from "chainlink-contracts/interfaces/VRFCoordinatorV2Interface.sol";
import {LinkTokenInterface} from "chainlink-contracts/interfaces/LinkTokenInterface.sol";
contract ProcessingPlant is
AccessControl,
ReentrancyGuard,
ERC1155Holder,
VRFConsumerBaseV2
{
/// -----------------------------------------------------------------------
/// Structs
/// -----------------------------------------------------------------------
struct ProcessGeode {
address owner;
uint256 tokenId;
}
struct ProcessingInfo {
uint256 startTime;
uint256 endTime;
}
struct RequestStatus {
bool fulfilled; // whether the request has been successfully fulfilled
bool exists; // whether a requestId exists
uint256[] randomWords;
}
/// -----------------------------------------------------------------------
/// Errors
/// -----------------------------------------------------------------------
error ProcessingPlant__AccountDoesNotOwnThatTokenId();
error ProcessingPlant__ProcessRoundIsNotOver();
error ProcessingPlant__PlantDoesNotCustodyAllTokenIds();
error ProcessingPlant__RequestIdDoesNotExist();
error ProcessingPlant__RequestIdUnfulfilled();
error ProcessingPlant__MismatchedArrayLengths();
error ProcessingPlant__IridiumRewardsCannotBeZero();
error ProcessingPlant__CannotDecrementByZero();
error ProcessingPlant__CannotDecrement();
/// -----------------------------------------------------------------------
/// Immutable parameters
/// -----------------------------------------------------------------------
VRFCoordinatorV2Interface immutable COORDINATOR;
LinkTokenInterface immutable LINKTOKEN;
bytes32 immutable s_keyHash;
uint64 immutable s_subscriptionId;
uint32 immutable s_callbackGasLimit = 100000;
uint16 immutable s_requestConfirmations = 3;
/// -----------------------------------------------------------------------
/// Storage variables
/// -----------------------------------------------------------------------
IridiumToken public iridium;
Geode public geode;
bytes32 public constant WHITELIST_DECREMENT_ROLE =
keccak256("WHITELIST_DECREMENT_ROLE");
uint256 public iridiumRewards;
uint256 public processRound;
uint256 public lastFinishedProcessRound;
/// @notice processRound => ProcessingInfo (startTime, endTime)
mapping(uint256 => ProcessingInfo) public roundInfo;
/// @notice processRound => tokenIds array
mapping(uint256 => uint256[]) public roundTokenIds;
/// @notice tokenId => owner address
mapping(uint256 => address) public rewardsFor;
/// @notice address => number exercisable whitelist spots
mapping(address => uint256) public exercisableWhitelistSpots;
/// @notice processRound => requestId
mapping(uint256 => uint256) public roundRequestId;
/// @notice requestId => RequestStatus
mapping(uint256 => RequestStatus) public s_requests;
/// -----------------------------------------------------------------------
/// Constructor
/// -----------------------------------------------------------------------
constructor(
IridiumToken iridium_,
Geode geode_,
uint64 subscriptionId,
address vrfCoordinator,
address link,
bytes32 keyHash
) VRFConsumerBaseV2(vrfCoordinator) {
_setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
iridium = iridium_;
geode = geode_;
COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
LINKTOKEN = LinkTokenInterface(link);
s_keyHash = keyHash;
s_subscriptionId = subscriptionId;
}
/// @notice Users with geodes transfer them to this contract for processing to earn additional rewards (iridium, whitelist spots).
/// @param tokenId geode tokenId to transfer and crack
function processGeode(uint256 tokenId) external nonReentrant {
if (geode.balanceOf(msg.sender, tokenId) == 0)
revert ProcessingPlant__AccountDoesNotOwnThatTokenId();
// Begin new processing round if the last one is over
if (block.timestamp > roundInfo[processRound].endTime) {
lastFinishedProcessRound = processRound;
processRound++;
roundInfo[processRound] = ProcessingInfo({
startTime: block.timestamp,
endTime: block.timestamp + 3 days
});
}
geode.safeTransferFrom(msg.sender, address(this), tokenId, 1, "");
roundTokenIds[processRound].push(tokenId);
rewardsFor[tokenId] = msg.sender;
}
/// -----------------------------------------------------------------------
/// Role actions: DEFAULT_ROLE_ADMIN, WHITELIST_DECREMENT_ROLE
/// -----------------------------------------------------------------------
// Consider using a MAX_BATCH_SIZE for `requestRandomness`
// Then track the next index to run request for in that processRound_
///@notice At the end of a process round, the admin can calls this function to make a chainlink VRF v2
/// requestRandomWords call. s_numWords should be equal to the number of tokenIds deposited in a given
/// processRound.
/// @dev Admin needs to use a Subscription method with Chainlink VRF as Direct Funding method maximum
/// random values is 10 (500 for sub).
/// @param processRound_ an expired process round
function requestRandomness(uint256 processRound_)
external
onlyRole(DEFAULT_ADMIN_ROLE)
returns (uint256 requestId)
{
if (block.timestamp < roundInfo[processRound_].endTime)
revert ProcessingPlant__ProcessRoundIsNotOver();
uint256[] memory balances = geode.balanceOfBatchSingleAddress(
address(this),
roundTokenIds[processRound_]
);
for (uint256 i; i < balances.length; ++i) {
if (balances[i] == 0)
revert ProcessingPlant__PlantDoesNotCustodyAllTokenIds();
}
uint32 s_numWords = uint32(roundTokenIds[processRound_].length);
requestId = COORDINATOR.requestRandomWords(
s_keyHash,
s_subscriptionId,
s_requestConfirmations,
s_callbackGasLimit,
s_numWords
);
roundRequestId[processRound_] = requestId;
s_requests[requestId] = RequestStatus({
randomWords: new uint256[](s_numWords),
exists: true,
fulfilled: false
});
}
/// @notice Chainlink VRF V2 callback function
function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords)
internal
override
{
if (!s_requests[requestId].exists)
revert ProcessingPlant__RequestIdDoesNotExist();
s_requests[requestId].fulfilled = true;
s_requests[requestId].randomWords = randomWords;
}
/// @notice Allocates random words to tokenIds, burns geodes and allocates rewards depending on random number
/// returned for the tokenId. If random number == 100, the owner of the tokenId will earn a whitelist spot +
/// iridium rewards.
/// @param processRound_ an expired process round
function crackGeodes(uint256 processRound_)
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
if (block.timestamp < roundInfo[processRound_].endTime)
revert ProcessingPlant__ProcessRoundIsNotOver();
if (iridiumRewards == 0)
revert ProcessingPlant__IridiumRewardsCannotBeZero();
uint256 requestId = roundRequestId[processRound_];
if (s_requests[requestId].fulfilled != true)
revert ProcessingPlant__RequestIdUnfulfilled();
uint256[] memory tokenIds_ = roundTokenIds[processRound_];
uint256[] memory words_ = getRandomWords(requestId);
if (tokenIds_.length != words_.length)
revert ProcessingPlant__MismatchedArrayLengths();
uint256[] memory values = new uint256[](tokenIds_.length);
for (uint256 i; i < values.length; ++i) {
values[i] = 1;
}
// Batch burn tokenIds
geode.burnBatch(address(this), tokenIds_, values);
// Allocate rewards
for (uint256 i; i < tokenIds_.length; ++i) {
address beneficiary = rewardsFor[tokenIds_[i]];
if (ranNum(words_[i]) == 100) {
exercisableWhitelistSpots[beneficiary]++;
}
iridium.mint(beneficiary, iridiumRewards);
}
}
/// @notice Sets iridium reward amount for cracking geodes
/// @param rewardAmount iridium reward amount
function setIridiumRewardAmount(uint256 rewardAmount)
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
iridiumRewards = rewardAmount;
}
/// @notice Updates iridium contract implementation (in the event that the project requires a V2 contract)
function updateIridiumImplementation(IridiumToken iridium_)
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
iridium = iridium_;
}
/// @notice Decrement exercisable whitelist spots by acounts with role: WHITELIST_DECREMENT_ROLE.
/// Enables future expansion NFT projects to decrement upon whitelist spot being exercised by user.
function decrementExercisableWhitelistSpots(address account, uint256 value)
external
onlyRole(WHITELIST_DECREMENT_ROLE)
{
if (value == 0) revert ProcessingPlant__CannotDecrementByZero();
uint256 currentSpots = exercisableWhitelistSpots[account];
if (currentSpots == 0) revert ProcessingPlant__CannotDecrement();
exercisableWhitelistSpots[account] = currentSpots - value;
}
/// -----------------------------------------------------------------------
/// Override
/// -----------------------------------------------------------------------
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(AccessControl, ERC1155Receiver)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
/// -----------------------------------------------------------------------
/// Helpers
/// -----------------------------------------------------------------------
function getRandomWords(uint256 _requestId)
public
view
returns (uint256[] memory words)
{
words = s_requests[_requestId].randomWords;
}
function ranNum(uint256 x) public pure returns (uint256 y) {
y = (x % 100) + 1;
}
}