forked from limitbreakinc/creator-token-standards
-
Notifications
You must be signed in to change notification settings - Fork 0
/
AdventureERC721.sol
437 lines (351 loc) · 19.7 KB
/
AdventureERC721.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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "./IAdventurous.sol";
import "./AdventureWhitelist.sol";
import "../token/erc721/ERC721OpenZeppelin.sol";
/**
* @title AdventureBase
* @author Limit Break, Inc.
* @notice Base functionality of the AdventureERC721 token standard.
*/
abstract contract AdventureBase is AdventureWhitelist, IAdventurous {
error AdventureERC721__AdventureApprovalToCaller();
error AdventureERC721__AlreadyOnQuest();
error AdventureERC721__AnActiveQuestIsPreventingTransfers();
error AdventureERC721__CallerNotApprovedForAdventure();
error AdventureERC721__CallerNotTokenOwner();
error AdventureERC721__MaxSimultaneousQuestsCannotBeZero();
error AdventureERC721__MaxSimultaneousQuestsExceeded();
error AdventureERC721__NotOnQuest();
error AdventureERC721__QuestIdOutOfRange();
error AdventureERC721__TooManyActiveQuests();
/// @notice Specifies an upper bound for the maximum number of simultaneous quests per adventure.
uint256 private constant MAX_CONCURRENT_QUESTS = 100;
/// @dev A value denoting a transfer originating from transferFrom or safeTransferFrom
uint256 internal constant TRANSFERRING_VIA_ERC721 = 1;
/// @dev A value denoting a transfer originating from adventureTransferFrom or adventureSafeTransferFrom
uint256 internal constant TRANSFERRING_VIA_ADVENTURE = 2;
/// @dev The most simultaneous quests the token may participate in at a time
uint256 private _maxSimultaneousQuests;
/// @dev Specifies the type of transfer that is actively being used
uint256 internal transferType;
/// @dev Maps each token id to the number of blocking quests it is currently entered into
mapping (uint256 => uint256) internal blockingQuestCounts;
/// @dev Mapping from owner to operator approvals for special gameplay behavior
mapping (address => mapping (address => bool)) private operatorAdventureApprovals;
/// @dev Maps each token id to a mapping that can enumerate all active quests within an adventure
mapping (uint256 => mapping (address => uint32[])) public activeQuestList;
/// @dev Maps each token id to a mapping from adventure address to a mapping of quest ids to quest details
mapping (uint256 => mapping (address => mapping (uint32 => Quest))) public activeQuestLookup;
/// @notice Transfers a player's token if they have opted into an authorized, whitelisted adventure.
function adventureTransferFrom(address from, address to, uint256 tokenId) external override {
_requireCallerIsWhitelistedAdventure();
_requireCallerApprovedForAdventure(tokenId);
transferType = TRANSFERRING_VIA_ADVENTURE;
_doTransfer(from, to, tokenId);
transferType = TRANSFERRING_VIA_ERC721;
}
/// @notice Safe transfers a player's token if they have opted into an authorized, whitelisted adventure.
function adventureSafeTransferFrom(address from, address to, uint256 tokenId) external override {
_requireCallerIsWhitelistedAdventure();
_requireCallerApprovedForAdventure(tokenId);
transferType = TRANSFERRING_VIA_ADVENTURE;
_doSafeTransfer(from, to, tokenId, "");
transferType = TRANSFERRING_VIA_ERC721;
}
/// @notice Burns a player's token if they have opted into an authorized, whitelisted adventure.
function adventureBurn(uint256 tokenId) external override {
_requireCallerIsWhitelistedAdventure();
_requireCallerApprovedForAdventure(tokenId);
transferType = TRANSFERRING_VIA_ADVENTURE;
_doBurn(tokenId);
transferType = TRANSFERRING_VIA_ERC721;
}
/// @notice Enters a player's token into a quest if they have opted into an authorized, whitelisted adventure.
function enterQuest(uint256 tokenId, uint256 questId) external override {
_requireCallerIsWhitelistedAdventure();
_requireCallerApprovedForAdventure(tokenId);
_enterQuest(tokenId, _msgSender(), questId);
}
/// @notice Exits a player's token from a quest if they have opted into an authorized, whitelisted adventure.
/// For developers of adventure contracts that perform adventure burns, be aware that the adventure must exitQuest
/// before the adventure burn occurs, as _exitQuest emits the owner of the token, which would revert after burning.
function exitQuest(uint256 tokenId, uint256 questId) external override {
_requireCallerIsWhitelistedAdventure();
_requireCallerApprovedForAdventure(tokenId);
_exitQuest(tokenId, _msgSender(), questId);
}
/// @notice Admin-only ability to boot a token from all quests on an adventure.
/// This ability is only unlocked in the event that an adventure has been unwhitelisted, as early exiting
/// from quests can cause out of sync state between the ERC721 token contract and the adventure/quest.
function bootFromAllQuests(uint256 tokenId, address adventure) external {
_requireCallerIsContractOwner();
_requireAdventureRemovedFromWhitelist(adventure);
_exitAllQuests(tokenId, adventure, true);
}
/// @notice Gives the player the ability to exit a quest without interacting directly with the approved, whitelisted adventure
/// This ability is only unlocked in the event that an adventure has been unwhitelisted, as early exiting
/// from quests can cause out of sync state between the ERC721 token contract and the adventure/quest.
function userExitQuest(uint256 tokenId, address adventure, uint256 questId) external {
_requireAdventureRemovedFromWhitelist(adventure);
_requireCallerOwnsToken(tokenId);
_exitQuest(tokenId, adventure, questId);
}
/// @notice Gives the player the ability to exit all quests on an adventure without interacting directly with the approved, whitelisted adventure
/// This ability is only unlocked in the event that an adventure has been unwhitelisted, as early exiting
/// from quests can cause out of sync state between the ERC721 token contract and the adventure/quest.
function userExitAllQuests(uint256 tokenId, address adventure) external {
_requireAdventureRemovedFromWhitelist(adventure);
_requireCallerOwnsToken(tokenId);
_exitAllQuests(tokenId, adventure, false);
}
/// @notice Similar to {IERC721-setApprovalForAll}, but for special in-game adventures only
function setAdventuresApprovedForAll(address operator, bool approved) external {
address tokenOwner = _msgSender();
if(tokenOwner == operator) {
revert AdventureERC721__AdventureApprovalToCaller();
}
operatorAdventureApprovals[tokenOwner][operator] = approved;
emit AdventureApprovalForAll(tokenOwner, operator, approved);
}
/// @notice Similar to {IERC721-isApprovedForAll}, but for special in-game adventures only
function areAdventuresApprovedForAll(address owner_, address operator) public view returns (bool) {
return operatorAdventureApprovals[owner_][operator];
}
/// @notice Returns the number of quests a token is actively participating in for a specified adventure
function getQuestCount(uint256 tokenId, address adventure) public override view returns (uint256) {
return activeQuestList[tokenId][adventure].length;
}
/// @notice Returns the amount of time a token has been participating in the specified quest
function getTimeOnQuest(uint256 tokenId, address adventure, uint256 questId) public override view returns (uint256) {
(bool participatingInQuest, uint256 startTimestamp,) = isParticipatingInQuest(tokenId, adventure, questId);
return participatingInQuest ? (block.timestamp - startTimestamp) : 0;
}
/// @notice Returns whether or not a token is currently participating in the specified quest as well as the time it was started and the quest index
function isParticipatingInQuest(uint256 tokenId, address adventure, uint256 questId) public override view returns (bool participatingInQuest, uint256 startTimestamp, uint256 index) {
if(questId > type(uint32).max) {
revert AdventureERC721__QuestIdOutOfRange();
}
Quest storage quest = activeQuestLookup[tokenId][adventure][uint32(questId)];
participatingInQuest = quest.isActive;
startTimestamp = quest.startTimestamp;
index = quest.arrayIndex;
return (participatingInQuest, startTimestamp, index);
}
/// @notice Returns a list of all active quests for the specified token id and adventure
function getActiveQuests(uint256 tokenId, address adventure) public override view returns (Quest[] memory activeQuests) {
uint256 questCount = getQuestCount(tokenId, adventure);
activeQuests = new Quest[](questCount);
uint32[] memory activeQuestIdList = activeQuestList[tokenId][adventure];
for(uint256 i = 0; i < questCount; ++i) {
activeQuests[i] = activeQuestLookup[tokenId][adventure][activeQuestIdList[i]];
}
return activeQuests;
}
function maxSimultaneousQuests() public virtual view returns (uint256) {
return _maxSimultaneousQuests;
}
/// @dev Enters the specified quest for a token id.
/// Throws if the token is already participating in the specified quest.
/// Throws if the number of active quests exceeds the max allowable for the given adventure.
/// Emits a QuestUpdated event for off-chain processing.
function _enterQuest(uint256 tokenId, address adventure, uint256 questId) internal {
(bool participatingInQuest,,) = isParticipatingInQuest(tokenId, adventure, questId);
if(participatingInQuest) {
revert AdventureERC721__AlreadyOnQuest();
}
uint256 currentQuestCount = getQuestCount(tokenId, adventure);
if(currentQuestCount >= maxSimultaneousQuests()) {
revert AdventureERC721__TooManyActiveQuests();
}
uint32 castedQuestId = uint32(questId);
activeQuestList[tokenId][adventure].push(castedQuestId);
activeQuestLookup[tokenId][adventure][castedQuestId].isActive = true;
activeQuestLookup[tokenId][adventure][castedQuestId].startTimestamp = uint64(block.timestamp);
activeQuestLookup[tokenId][adventure][castedQuestId].questId = castedQuestId;
activeQuestLookup[tokenId][adventure][castedQuestId].arrayIndex = uint32(currentQuestCount);
address ownerOfToken = _ownerOfToken(tokenId);
emit QuestUpdated(tokenId, ownerOfToken, adventure, questId, true, false);
if(IAdventure(adventure).questsLockTokens()) {
unchecked {
++blockingQuestCounts[tokenId];
}
}
// Invoke callback to the adventure to facilitate state synchronization as needed
IAdventure(adventure).onQuestEntered(ownerOfToken, tokenId, questId);
}
/// @dev Exits the specified quest for a token id.
/// Throws if the token is not currently participating on the specified quest.
/// Emits a QuestUpdated event for off-chain processing.
function _exitQuest(uint256 tokenId, address adventure, uint256 questId) internal {
(bool participatingInQuest, uint256 startTimestamp, uint256 index) = isParticipatingInQuest(tokenId, adventure, questId);
if(!participatingInQuest) {
revert AdventureERC721__NotOnQuest();
}
uint32 castedQuestId = uint32(questId);
uint256 lastArrayIndex = getQuestCount(tokenId, adventure) - 1;
if(index != lastArrayIndex) {
activeQuestList[tokenId][adventure][index] = activeQuestList[tokenId][adventure][lastArrayIndex];
activeQuestLookup[tokenId][adventure][activeQuestList[tokenId][adventure][lastArrayIndex]].arrayIndex = uint32(index);
}
activeQuestList[tokenId][adventure].pop();
delete activeQuestLookup[tokenId][adventure][castedQuestId];
address ownerOfToken = _ownerOfToken(tokenId);
emit QuestUpdated(tokenId, ownerOfToken, adventure, questId, false, false);
if(IAdventure(adventure).questsLockTokens()) {
--blockingQuestCounts[tokenId];
}
// Invoke callback to the adventure to facilitate state synchronization as needed
IAdventure(adventure).onQuestExited(ownerOfToken, tokenId, questId, startTimestamp);
}
/// @dev Removes the specified token id from all quests on the specified adventure
function _exitAllQuests(uint256 tokenId, address adventure, bool booted) internal {
address tokenOwner = _ownerOfToken(tokenId);
uint256 questCount = getQuestCount(tokenId, adventure);
if(IAdventure(adventure).questsLockTokens()) {
blockingQuestCounts[tokenId] -= questCount;
}
for(uint256 i = 0; i < questCount;) {
uint32 questId = activeQuestList[tokenId][adventure][i];
Quest memory quest = activeQuestLookup[tokenId][adventure][questId];
uint256 startTimestamp = quest.startTimestamp;
emit QuestUpdated(tokenId, tokenOwner, adventure, questId, false, booted);
delete activeQuestLookup[tokenId][adventure][questId];
// Invoke callback to the adventure to facilitate state synchronization as needed
IAdventure(adventure).onQuestExited(tokenOwner, tokenId, questId, startTimestamp);
unchecked {
++i;
}
}
delete activeQuestList[tokenId][adventure];
}
/// @dev Validates that the caller is approved for adventure on the specified token id
/// Throws when the caller has not been approved by the user.
function _requireCallerApprovedForAdventure(uint256 tokenId) internal view {
if(!areAdventuresApprovedForAll(_ownerOfToken(tokenId), _msgSender())) {
revert AdventureERC721__CallerNotApprovedForAdventure();
}
}
/// @dev Validates that the caller owns the specified token
/// Throws when the caller does not own the specified token.
function _requireCallerOwnsToken(uint256 tokenId) internal view {
if(_ownerOfToken(tokenId) != _msgSender()) {
revert AdventureERC721__CallerNotTokenOwner();
}
}
/// @dev Validates that the specified value of max simultaneous quests is in range [1-MAX_CONCURRENT_QUESTS]
/// Throws when `maxSimultaneousQuests_` is zero.
/// Throws when `maxSimultaneousQuests_` is more than MAX_CONCURRENT_QUESTS.
function _validateMaxSimultaneousQuests(uint256 maxSimultaneousQuests_) internal pure {
if(maxSimultaneousQuests_ == 0) {
revert AdventureERC721__MaxSimultaneousQuestsCannotBeZero();
}
if(maxSimultaneousQuests_ > MAX_CONCURRENT_QUESTS) {
revert AdventureERC721__MaxSimultaneousQuestsExceeded();
}
}
function _setMaxSimultaneousQuestsAndInitializeTransferType(uint256 maxSimultaneousQuests_) internal {
_validateMaxSimultaneousQuests(maxSimultaneousQuests_);
_maxSimultaneousQuests = maxSimultaneousQuests_;
transferType = TRANSFERRING_VIA_ERC721;
}
function _doBurn(uint256 tokenId) internal virtual;
function _doTransfer(address from, address to, uint256 tokenId) internal virtual;
function _doSafeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal virtual;
function _ownerOfToken(uint256 tokenId) internal view virtual returns (address);
}
/**
* @title AdventureERC721
* @author Limit Break, Inc.
* @notice Standard AdventureERC721 implementation allowing for constructor to be called
*/
abstract contract AdventureERC721 is AdventureBase, ERC721OpenZeppelin {
/// @dev The most simultaneous quests the token may participate in at a time
uint256 private immutable _maxSimultaneousQuestsImmutable;
constructor(uint256 maxSimultaneousQuests_) {
_setMaxSimultaneousQuestsAndInitializeTransferType(maxSimultaneousQuests_);
_maxSimultaneousQuestsImmutable = maxSimultaneousQuests_;
}
/// @dev ERC-165 interface support
function supportsInterface(bytes4 interfaceId) public view virtual override (ERC721, IERC165) returns (bool) {
return
interfaceId == type(IAdventurous).interfaceId ||
super.supportsInterface(interfaceId);
}
function maxSimultaneousQuests() public view override returns (uint256) {
return _maxSimultaneousQuestsImmutable;
}
function _doBurn(uint256 tokenId) internal virtual override {
_burn(tokenId);
}
function _doTransfer(address from, address to, uint256 tokenId) internal virtual override {
_transfer(from, to, tokenId);
}
function _doSafeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal virtual override {
_safeTransfer(from, to, tokenId, data);
}
function _ownerOfToken(uint256 tokenId) internal view virtual override returns (address) {
return ownerOf(tokenId);
}
/// @dev By default, tokens that are participating in quests are transferrable. However, if a token is participating
/// in a quest on an adventure that was designated as a token locker, the transfer will revert and keep the token
/// locked.
function _beforeTokenTransfer(address /*from*/, address /*to*/, uint256 firstTokenId, uint256 batchSize) internal virtual override {
for (uint256 i = 0; i < batchSize;) {
if(blockingQuestCounts[firstTokenId + i] > 0) {
revert AdventureERC721__AnActiveQuestIsPreventingTransfers();
}
unchecked {
++i;
}
}
}
}
/**
* @title AdventureERC721Initializable
* @author Limit Break, Inc.
* @notice Initializable AdventureERC721 implementation allowing for EIP-1167 clones.
*/
abstract contract AdventureERC721Initializable is AdventureBase, ERC721OpenZeppelinInitializable {
error AdventureERC721Initializable__AlreadyInitializedMaxSimultaneousQuestsAndTransferType();
bool private _maxSimultaneousQuestsInitialized;
function initializeMaxSimultaneousQuestsAndTransferType(uint256 maxSimultaneousQuests_) public {
_requireCallerIsContractOwner();
if(_maxSimultaneousQuestsInitialized) {
revert AdventureERC721Initializable__AlreadyInitializedMaxSimultaneousQuestsAndTransferType();
}
_maxSimultaneousQuestsInitialized = true;
_setMaxSimultaneousQuestsAndInitializeTransferType(maxSimultaneousQuests_);
}
/// @dev ERC-165 interface support
function supportsInterface(bytes4 interfaceId) public view virtual override (ERC721, IERC165) returns (bool) {
return
interfaceId == type(IAdventurous).interfaceId ||
super.supportsInterface(interfaceId);
}
function _doBurn(uint256 tokenId) internal virtual override {
_burn(tokenId);
}
function _doTransfer(address from, address to, uint256 tokenId) internal virtual override {
_transfer(from, to, tokenId);
}
function _doSafeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal virtual override {
_safeTransfer(from, to, tokenId, data);
}
function _ownerOfToken(uint256 tokenId) internal view virtual override returns (address) {
return ownerOf(tokenId);
}
/// @dev By default, tokens that are participating in quests are transferrable. However, if a token is participating
/// in a quest on an adventure that was designated as a token locker, the transfer will revert and keep the token
/// locked.
function _beforeTokenTransfer(address /*from*/, address /*to*/, uint256 firstTokenId, uint256 batchSize) internal virtual override {
for (uint256 i = 0; i < batchSize;) {
if(blockingQuestCounts[firstTokenId + i] > 0) {
revert AdventureERC721__AnActiveQuestIsPreventingTransfers();
}
unchecked {
++i;
}
}
}
}