-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathGenesisCrafter.sol
369 lines (319 loc) · 15.8 KB
/
GenesisCrafter.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
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.24;
// ** ** ** ** **** **
// /** /** /** // /**/ /**
// /** /** /** ** ****** ****** ****** ******
// /** /** /****** /** **//// **////**///**/ ///**/
// /** /** /**///** /**//***** /** /** /** /**
// /** /** /** /** /** /////**/** /** /** /**
// //******* /****** /** ****** //****** /** //**
// /////// ///// // ////// ////// // //
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {GenesisUpgradeable} from "src/abstracts/GenesisUpgradeable.sol";
import {IGenesisChampion} from "src/interfaces/IGenesisChampion.sol";
import {IGenesisChampionFactory} from "src/interfaces/IGenesisChampionFactory.sol";
import {IGenesisCrafter} from "src/interfaces/IGenesisCrafter.sol";
import {IGenesisCrafterRule} from "src/interfaces/IGenesisCrafterRule.sol";
import {Errors} from "src/librairies/Errors.sol";
import {CraftCounter} from "src/types/CraftCounter.sol";
import {CraftData} from "src/types/CraftData.sol";
/**
* @title GenesisCrafter
*
* @notice GenesisCrafter is a UUPS proxy implementing crafting mechanisms & rules for all GenesisChampion tokens
*/
contract GenesisCrafter is IGenesisCrafter, GenesisUpgradeable {
using SafeERC20 for IERC20;
// =============================================================
// EVENTS
// =============================================================
/// @notice emitted after a successful crafting
event Craft(
address indexed childCollection,
bytes32 indexed craftNonce,
uint256 indexed childId,
address collectionA,
address collectionB,
uint256 parentA,
uint256 parentB
);
/// @notice emitted when vault has changed
event VaultUpdate(address vault);
/// @notice emitted when individual crafter rule is set
event SetCrafterRule(address crafterRule, address collection, uint256 id);
/// @notice emitted when craft fees are paid
event CraftFees(address vault, address indexed currency, uint256 amount, address from);
// =============================================================
// MODIFIERS
// =============================================================
/// @dev CraftData parameters validation before crafting
modifier validRequestParams(CraftData calldata request) {
// Recipient is address(0)
if (request.to == address(0)) revert Errors.ZeroAddress();
// payment_value is address(0) (soft currency) but value was passed
if (request.payment_type == address(0) && request.payment_value > 0) revert Errors.WantSoftGotToken();
// Parents must be two different entities
if (request.parent_a == request.parent_b && request.collection_a == request.collection_b) {
revert Errors.CraftWithSameParents(request.collection_a, request.parent_a);
}
_;
}
/// @dev verify the craft counts for parent A/B match the new craft counts computed by back-end
modifier craftCounterEqualsAsExpected(CraftData calldata request) {
_;
if (craftCounters[request.collection_a][request.parent_a].craftCount != request.expected_cc_a) {
revert Errors.UnexpectedCraftCount(request.collection_a, request.parent_a);
}
if (craftCounters[request.collection_b][request.parent_b].craftCount != request.expected_cc_b) {
revert Errors.UnexpectedCraftCount(request.collection_b, request.parent_b);
}
}
// =============================================================
// CONSTANTS
// =============================================================
/// @notice Minter role used for AccessControl
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
// =============================================================
// MAPPINGS
// =============================================================
/**
* @notice Crafting Rules define the number of craft counts a Champion can use
* These rules are applied in {_validateCraftConditions} for all Champions
* Collection-wide or token-specifc crafting rules can be applied using this `craftRule` mapping
* Collection-wide policies are defined in `craftRule[collection][0]` as ID 0 can never be minted
* Token-specific policies are defined in `craftRule[collection][id]`
* Token-specific policies take precedence over Collection-wide policies;
*/
mapping(address collection => mapping(uint256 tokenId => address crafterRuleAddress)) public craftRule;
/// @notice craftCounters holds the CraftCounter data of all tokenIds of any deployed GenesisChampion collections
mapping(address collection => mapping(uint256 tokenId => CraftCounter tokenCraftCounter)) public craftCounters;
// =============================================================
// VARIABLES
// =============================================================
/// @notice GenesisChampionFactory contract instance
IGenesisChampionFactory factory;
/// @notice wallet receiving crafting fees
address public vault;
/// @notice Storage gap for future upgrades
uint256[46] __gap;
// =============================================================
// UUPS
// =============================================================
function initialize(address factory_, address crafter_, address vault_) external initializer {
// Initialize upgradeable contracts
__Ownable_init(_msgSender());
__AccessControl_init();
__UUPSUpgradeable_init();
// Grant DEFAULT_ADMIN_ROLE to owner()
_grantRole(DEFAULT_ADMIN_ROLE, _msgSender());
// Grant MINTER_ROLE to crafter_
_grantRole(MINTER_ROLE, crafter_);
// Instantiate the factory
factory = IGenesisChampionFactory(factory_);
// Setup the vault;
vault = vault_;
}
// =============================================================
// EXTERNAL
// =============================================================
/**
* @inheritdoc IGenesisCrafter
*/
function craft(CraftData calldata request)
external
validRequestParams(request)
onlyRole(MINTER_ROLE)
craftCounterEqualsAsExpected(request)
{
(uint256 newMaxCraftCount, address refParentAddress) = _determineBestParent(
request.collection_a, request.parent_a, request.collection_b, request.parent_b, request.to
);
// Determine the payment type used and process payment if needed
if (request.payment_type != address(0)) {
_processCraftFee(request.payment_type, request.payment_value, request.to, request.payer);
}
// Consume craftCounter for each parent
craftCounters[request.collection_a][request.parent_a].craftCount++;
craftCounters[request.collection_b][request.parent_b].craftCount++;
// Update the lock period
if (request.lockPeriod > 0) {
craftCounters[request.collection_a][request.parent_a].lockedUntil = block.timestamp + request.lockPeriod;
craftCounters[request.collection_b][request.parent_b].lockedUntil = block.timestamp + request.lockPeriod;
}
// Mint a new champion and initialize its craftCounter
(uint256 tokenId,) = IGenesisChampion(refParentAddress).mint(request.to, 1);
craftCounters[refParentAddress][tokenId].maxCraftCount = newMaxCraftCount;
craftCounters[refParentAddress][tokenId].initialized = true;
emit Craft(
refParentAddress,
request.nonce,
tokenId,
request.collection_a,
request.collection_b,
request.parent_a,
request.parent_b
);
}
/**
* @inheritdoc IGenesisCrafter
*/
function setCrafterRule(address crafterRule, address collection, uint256 id) external onlyOwner {
craftRule[collection][id] = crafterRule;
emit SetCrafterRule(crafterRule, collection, id);
}
/**
* @inheritdoc IGenesisCrafter
*/
function viewMaxCraftCount(address collection, uint256 id) external returns (uint256) {
if (craftCounters[collection][id].initialized == false) {
return IGenesisChampion(collection).defaultMaxCraftCount();
}
return craftCounters[collection][id].maxCraftCount;
}
/**
* @inheritdoc GenesisUpgradeable
*/
function version() external pure override returns (uint256) {
return 1;
}
/**
* @inheritdoc IGenesisCrafter
*/
function updateVault(address newVault) external onlyOwner {
vault = newVault;
emit VaultUpdate(vault);
}
// =============================================================
// INTERNAL
// =============================================================
/**
* @notice _determineBestParent verifies which parent is the oldest
* @param collection_a address of collection_a
* @param parent_a tokenId of parent_a
* @param collection_b address of collection_b
* @param parent_b tokenId of parent_b
* @param to user receiving the token
*/
function _determineBestParent(
address collection_a,
uint256 parent_a,
address collection_b,
uint256 parent_b,
address to
) internal returns (uint256, address) {
// parent_a can craft
(uint256 versionA, uint256 maxCraftCountA) = _validateCraftConditions(collection_a, parent_a, to);
// parent_b can craft
(uint256 versionB, uint256 maxCraftCountB) = _validateCraftConditions(collection_b, parent_b, to);
// Collection to mint from is determined by the oldest deployed contract
// or biggest max craft count if both parents belong to the same contract
address refParentAddress;
uint256 refMaxCraftCount;
if (collection_a == collection_b) {
// Craft from the same collection, use maxCraftCount{A|B} to determine oldest parent
refParentAddress = collection_a;
refMaxCraftCount = maxCraftCountB > maxCraftCountA ? maxCraftCountB : maxCraftCountA;
} else {
// Craft from two different collections, use version{A|B} to determine oldest collection and maxCraftCount
refParentAddress = versionB < versionA ? collection_b : collection_a;
refMaxCraftCount = versionB < versionA ? maxCraftCountB : maxCraftCountA;
}
// maxCraftCount for the new Champion comes from the oldest parent's maxCraftCount - 1
// if a CrafterRule is set, refMaxCraftCount can eventually be 0 so we need to catch the underflow
// refMaxCraftCount can never be 0, else will revert
uint256 newMaxCraftCount = refMaxCraftCount - 1;
return (newMaxCraftCount, refParentAddress);
}
/**
* @notice _validateCraftConditions verifies the current craftCount, maxCraftCount and lockPeriod for a specific _collection and _id
* and checks if special crafting rules should apply at a collection-wide or token-specific level
* @dev returns (uint256 deploymentIndex, uint256 maxCraftCount)
* @param collection address of the parent token
* @param id of the parent token
* @param owner_ of the token
*/
function _validateCraftConditions(address collection, uint256 id, address owner_)
internal
returns (uint256, uint256)
{
// Collections was deployed by the factory
uint256 index = factory.deployedVersions(collection);
if (index == 0) revert Errors.CollectionUnknown(collection);
// Verify ownership
IGenesisChampion champ = IGenesisChampion(collection);
if (champ.ownerOf(id) != owner_) revert Errors.CallerNotOwner(collection, id);
// Initialize the parent during its first craft
_initializeChampionIfRequired(collection, id);
uint256 maxCraftCount = craftCounters[collection][id].maxCraftCount;
// Craft can be locked for a certain period of time if specified in the lastest craft request
if (block.timestamp < craftCounters[collection][id].lockedUntil) revert Errors.ParentCraftLock();
// Enforce collection-wide or token-specific crafting rules, if applicable
// type(uint256).max means no override
uint256 specialMaxCraftCount = type(uint256).max;
address rule = craftRule[collection][id];
uint256 ruleId = id;
// Apply collection-wide rule if no individual rule is specified
if (rule == address(0)) {
rule = craftRule[collection][0];
ruleId = 0;
}
if (rule != address(0)) {
IGenesisCrafterRule ruleImpl = IGenesisCrafterRule(rule);
specialMaxCraftCount = ruleImpl.validateCraft(collection, ruleId);
// check craftCount, specialMaxCraftCount overrides maxCraftCount if applicable
if (
specialMaxCraftCount != type(uint256).max
&& craftCounters[collection][id].craftCount >= specialMaxCraftCount
) revert Errors.MaxCraftCount(collection, id, specialMaxCraftCount);
}
// specialMaxCraftCount doesn't apply, use the registered craftCounters for [collection][id]
if (specialMaxCraftCount == type(uint256).max && craftCounters[collection][id].craftCount >= maxCraftCount) {
revert Errors.MaxCraftCount(collection, id, maxCraftCount);
}
// return maxCraftCount, we don't want to craft champions with a maxCraftCount based on an external rule
return (index, maxCraftCount);
}
/**
* @notice determine what currency is used to pay crafting fees and process the payment
* @dev emits `CraftFee(address, address, uint256, address)` on success, except for soft currency crafts
* @param currencyAddress address of the payment token
* @param value craft fee to pay
* @param crafter address of the user sending the craft request
* @param payer if applicable, address paying for the operation
*/
function _processCraftFee(address currencyAddress, uint256 value, address crafter, address payer) internal {
if (payer != address(0)) _processERC20Payment(payer, currencyAddress, value);
else _processERC20Payment(crafter, currencyAddress, value);
}
/**
* @notice Pay craft fees with `ERC20.transferFrom` on behalf of the user
* @dev requires the user's approval
* @param payer address of the crafter
* @param token address of the ERC20 used as currency
* @param value amount to transfer
*/
function _processERC20Payment(address payer, address token, uint256 value) internal {
IERC20 erc20 = IERC20(token);
// Balance before calling transferFrom
uint256 receiverBalanceBefore = erc20.balanceOf(vault);
erc20.safeTransferFrom(payer, vault, value);
// Balances after calling transferFrom
uint256 receiverBalanceAfter = erc20.balanceOf(vault);
if (receiverBalanceAfter != (receiverBalanceBefore + value)) {
revert Errors.TransferCraftFees(token, value, vault);
}
emit CraftFees(vault, token, value, payer);
}
/**
* @dev initialize a champion with its max craft count
* @param collection address of the champion's contract
* @param id of the champion
*/
function _initializeChampionIfRequired(address collection, uint256 id) internal {
if (!craftCounters[collection][id].initialized) {
craftCounters[collection][id].initialized = true;
craftCounters[collection][id].maxCraftCount = IGenesisChampion(collection).defaultMaxCraftCount();
}
}
}