@totems/evm 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +261 -0
- package/README.md +28 -0
- package/contracts/Errors.sol +29 -0
- package/contracts/ModMarket.sol +467 -0
- package/contracts/ReentrancyGuard.sol +17 -0
- package/contracts/Shared.sol +45 -0
- package/contracts/Totems.sol +835 -0
- package/interfaces/IMarket.sol +153 -0
- package/interfaces/IRelayFactory.sol +13 -0
- package/interfaces/ITotemTypes.sol +200 -0
- package/interfaces/ITotems.sol +178 -0
- package/mods/TotemMod.sol +136 -0
- package/mods/TotemsLibrary.sol +161 -0
- package/package.json +35 -0
- package/test/helpers.ts +466 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
import "../shared/ReentrancyGuard.sol";
|
|
5
|
+
import "../library/ITotemTypes.sol";
|
|
6
|
+
import "../shared/Shared.sol";
|
|
7
|
+
import "../totems/Errors.sol";
|
|
8
|
+
import {IMod} from "../interfaces/TotemMod.sol";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @title ModMarket
|
|
12
|
+
* @author Totems
|
|
13
|
+
* @notice On-chain marketplace for publishing and discovering Totems mods
|
|
14
|
+
* @dev Mods are smart contracts that extend totem functionality through hooks.
|
|
15
|
+
* Developers deploy a mod contract, then publish it here with metadata.
|
|
16
|
+
* The marketplace handles:
|
|
17
|
+
* - Publishing with validation (name, summary, hooks, etc.)
|
|
18
|
+
* - Fee collection with referrer support and burn mechanism
|
|
19
|
+
* - Mod discovery via pagination and batch queries
|
|
20
|
+
* - Required actions that must be called after totem creation
|
|
21
|
+
*/
|
|
22
|
+
contract ModMarket is ReentrancyGuard {
|
|
23
|
+
|
|
24
|
+
// ==================== STATE VARIABLES ====================
|
|
25
|
+
|
|
26
|
+
/// @notice Referrer fee amounts by address (must be >= minBaseFee)
|
|
27
|
+
mapping(address => uint256) internal fees;
|
|
28
|
+
|
|
29
|
+
/// @notice Published mod data by contract address
|
|
30
|
+
mapping(address => ITotemTypes.Mod) internal mods;
|
|
31
|
+
|
|
32
|
+
/// @notice Required setup actions for each mod (called after totem creation)
|
|
33
|
+
mapping(address => ITotemTypes.ModRequiredAction[]) internal modRequiredActions;
|
|
34
|
+
|
|
35
|
+
/// @notice Ordered list of all published mod addresses (for pagination)
|
|
36
|
+
address[] public modList;
|
|
37
|
+
|
|
38
|
+
/// @notice Minimum base fee for mod publishing (set once in constructor, never changed)
|
|
39
|
+
uint256 public immutable minBaseFee;
|
|
40
|
+
|
|
41
|
+
/// @notice Fee amount that is always burned (set once in constructor, never changed)
|
|
42
|
+
uint256 public immutable burnedFee;
|
|
43
|
+
|
|
44
|
+
// ==================== CONSTRUCTOR ====================
|
|
45
|
+
|
|
46
|
+
/// @notice Deploys the market with the minimum base fee and burned fee
|
|
47
|
+
/// @param _minBaseFee Minimum base fee for mod publishing
|
|
48
|
+
/// @param _burnedFee Fee amount that is always burned
|
|
49
|
+
constructor(uint256 _minBaseFee, uint256 _burnedFee) {
|
|
50
|
+
require(_minBaseFee > 0, "Invalid min base fee");
|
|
51
|
+
require(_burnedFee <= _minBaseFee, "Burned fee cannot exceed min base fee");
|
|
52
|
+
minBaseFee = _minBaseFee;
|
|
53
|
+
burnedFee = _burnedFee;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ==================== EVENTS ====================
|
|
57
|
+
|
|
58
|
+
/// @notice Emitted when a new mod is published to the marketplace
|
|
59
|
+
/// @param mod The address of the published mod contract
|
|
60
|
+
event ModPublished(address indexed mod);
|
|
61
|
+
|
|
62
|
+
/// @notice Emitted when a mod's details, price, or required actions are updated
|
|
63
|
+
/// @param mod The address of the updated mod contract
|
|
64
|
+
event ModUpdated(address indexed mod);
|
|
65
|
+
|
|
66
|
+
// ==================== ERRORS ====================
|
|
67
|
+
|
|
68
|
+
/// @notice Caller is not authorized (not the mod seller)
|
|
69
|
+
error Unauthorized();
|
|
70
|
+
|
|
71
|
+
/// @notice Mod contract has already been published
|
|
72
|
+
error ModAlreadyPublished(address mod);
|
|
73
|
+
|
|
74
|
+
/// @notice Mod contract not found in marketplace
|
|
75
|
+
error ModNotFound(address mod);
|
|
76
|
+
|
|
77
|
+
/// @notice Hook identifier is not valid
|
|
78
|
+
error InvalidHook(ITotemTypes.Hook hook);
|
|
79
|
+
|
|
80
|
+
/// @notice Mod name cannot be empty
|
|
81
|
+
error EmptyModName();
|
|
82
|
+
|
|
83
|
+
/// @notice Mod summary cannot be empty
|
|
84
|
+
error EmptyModSummary();
|
|
85
|
+
|
|
86
|
+
/// @notice Mod name exceeds 100 character limit
|
|
87
|
+
error ModNameTooLong(uint256 length);
|
|
88
|
+
|
|
89
|
+
/// @notice Mod name is shorter than 3 characters
|
|
90
|
+
error ModNameTooShort(uint256 length);
|
|
91
|
+
|
|
92
|
+
/// @notice Mod summary exceeds 150 character limit
|
|
93
|
+
error ModSummaryTooLong(uint256 length);
|
|
94
|
+
|
|
95
|
+
/// @notice Mod summary is shorter than 10 characters
|
|
96
|
+
error ModSummaryTooShort(uint256 length);
|
|
97
|
+
|
|
98
|
+
/// @notice Mod image URL cannot be empty
|
|
99
|
+
error EmptyModImage();
|
|
100
|
+
|
|
101
|
+
/// @notice At least one hook must be specified
|
|
102
|
+
error NoHooksSpecified();
|
|
103
|
+
|
|
104
|
+
/// @notice Mod address is zero or not a contract
|
|
105
|
+
error InvalidContractAddress();
|
|
106
|
+
|
|
107
|
+
/// @notice Same hook specified multiple times
|
|
108
|
+
error DuplicateHook(ITotemTypes.Hook hook);
|
|
109
|
+
|
|
110
|
+
/// @notice Payment is less than required fee
|
|
111
|
+
error InsufficientFee(uint256 required, uint256 provided);
|
|
112
|
+
|
|
113
|
+
// ==================== EXTERNAL FUNCTIONS ====================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @notice Set your referrer fee for mod publishing
|
|
117
|
+
* @dev Referrers earn fees when users publish mods using their address.
|
|
118
|
+
* Fee must be at least minBaseFee. The referrer receives (fee - burnedFee).
|
|
119
|
+
* @param fee The fee amount in wei (must be >= minBaseFee)
|
|
120
|
+
*/
|
|
121
|
+
function setReferrerFee(uint256 fee) external {
|
|
122
|
+
if(fee < minBaseFee){
|
|
123
|
+
revert Errors.ReferrerFeeTooLow(minBaseFee);
|
|
124
|
+
}
|
|
125
|
+
fees[msg.sender] = fee;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @notice Get the fee for creating a totem or publishing a mod
|
|
130
|
+
* @param referrer The referrer address (or zero address for no referrer)
|
|
131
|
+
* @return The total fee required (at least minBaseFee)
|
|
132
|
+
*/
|
|
133
|
+
function getFee(address referrer) external view returns (uint256) {
|
|
134
|
+
if (referrer == address(0)) {
|
|
135
|
+
return minBaseFee;
|
|
136
|
+
}
|
|
137
|
+
uint256 referrerFee = fees[referrer];
|
|
138
|
+
return referrerFee > minBaseFee ? referrerFee : minBaseFee;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @notice Publish a mod to the marketplace
|
|
143
|
+
* @dev Only the mod's seller (returned by mod.getSeller()) can publish.
|
|
144
|
+
* Validates: contract exists, not already published, name 3-100 chars,
|
|
145
|
+
* summary 10-150 chars, image URL present, valid hooks with no duplicates.
|
|
146
|
+
* Excess payment is refunded to sender.
|
|
147
|
+
* @param mod Deployed mod contract address
|
|
148
|
+
* @param hooks Array of supported hook identifiers (Created, Mint, Burn, Transfer, TransferOwnership)
|
|
149
|
+
* @param price Price in wei to use this mod (paid when totem creator adds the mod)
|
|
150
|
+
* @param details Mod display details (name, summary, image, website, etc.)
|
|
151
|
+
* @param requiredActions Setup actions users must call after totem creation
|
|
152
|
+
* @param referrer Optional referrer address (receives fee minus burned amount)
|
|
153
|
+
*/
|
|
154
|
+
function publish(
|
|
155
|
+
address mod,
|
|
156
|
+
ITotemTypes.Hook[] calldata hooks,
|
|
157
|
+
uint256 price,
|
|
158
|
+
ITotemTypes.ModDetails calldata details,
|
|
159
|
+
ITotemTypes.ModRequiredAction[] calldata requiredActions,
|
|
160
|
+
address payable referrer
|
|
161
|
+
) external payable nonReentrant {
|
|
162
|
+
if (mod == address(0)) revert InvalidContractAddress();
|
|
163
|
+
if (mod.code.length == 0) revert InvalidContractAddress();
|
|
164
|
+
if (mods[mod].mod != address(0)) {
|
|
165
|
+
revert ModAlreadyPublished(mod);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
IMod modContract = IMod(mod);
|
|
169
|
+
address payable seller = modContract.getSeller();
|
|
170
|
+
if (seller != msg.sender) {
|
|
171
|
+
revert Unauthorized();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_validateModDetails(details);
|
|
175
|
+
if (hooks.length == 0) revert NoHooksSpecified();
|
|
176
|
+
|
|
177
|
+
for (uint256 i = 0; i < hooks.length; i++) {
|
|
178
|
+
if (!_isValidHook(hooks[i])) {
|
|
179
|
+
revert InvalidHook(hooks[i]);
|
|
180
|
+
}
|
|
181
|
+
for (uint256 j = i + 1; j < hooks.length; j++) {
|
|
182
|
+
if (hooks[i] == hooks[j]) {
|
|
183
|
+
revert DuplicateHook(hooks[i]);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Process fees and validate payment
|
|
189
|
+
uint256 totalFee = _processPublishFees(referrer);
|
|
190
|
+
|
|
191
|
+
// Store mod
|
|
192
|
+
ITotemTypes.Mod storage modData = mods[mod];
|
|
193
|
+
modData.mod = mod;
|
|
194
|
+
modData.seller = seller;
|
|
195
|
+
modData.price = price;
|
|
196
|
+
modData.details = details;
|
|
197
|
+
modData.hooks = hooks;
|
|
198
|
+
modData.publishedAt = uint64(block.timestamp);
|
|
199
|
+
modData.updatedAt = uint64(block.timestamp);
|
|
200
|
+
|
|
201
|
+
// Store required actions separately
|
|
202
|
+
for (uint256 i = 0; i < requiredActions.length; i++) {
|
|
203
|
+
modRequiredActions[mod].push(requiredActions[i]);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
modList.push(mod);
|
|
207
|
+
|
|
208
|
+
// Refund excess payment
|
|
209
|
+
if (msg.value > totalFee) {
|
|
210
|
+
Shared.safeTransfer(msg.sender, msg.value - totalFee);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
emit ModPublished(mod);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @notice Process publish fees - validates payment and distributes funds
|
|
218
|
+
* @dev Fee distribution:
|
|
219
|
+
* - No referrer: entire fee is burned (sent to address(0))
|
|
220
|
+
* - With referrer: burnedFee is burned, referrer gets (totalFee - burnedFee)
|
|
221
|
+
* The totalFee is max(referrer's fee, minBaseFee).
|
|
222
|
+
* @param referrer Optional referrer address (zero address = no referrer)
|
|
223
|
+
* @return totalFee The total fee charged
|
|
224
|
+
*/
|
|
225
|
+
function _processPublishFees(address payable referrer) internal returns (uint256 totalFee) {
|
|
226
|
+
uint256 referrerFee = referrer != address(0) ? fees[referrer] : 0;
|
|
227
|
+
totalFee = referrerFee > minBaseFee ? referrerFee : minBaseFee;
|
|
228
|
+
|
|
229
|
+
if (msg.value < totalFee) {
|
|
230
|
+
revert InsufficientFee(totalFee, msg.value);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (referrer == address(0)) {
|
|
234
|
+
// No referrer - burn the entire fee
|
|
235
|
+
Shared.safeTransfer(address(0), totalFee);
|
|
236
|
+
} else {
|
|
237
|
+
// burnedFee is always burned
|
|
238
|
+
if (burnedFee > 0) {
|
|
239
|
+
Shared.safeTransfer(address(0), burnedFee);
|
|
240
|
+
}
|
|
241
|
+
// Referrer gets the difference between totalFee and burnedFee
|
|
242
|
+
uint256 referrerAmount = totalFee - burnedFee;
|
|
243
|
+
if (referrerAmount > 0) {
|
|
244
|
+
Shared.safeTransfer(referrer, referrerAmount);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @notice Update mod price and details
|
|
251
|
+
* @param mod Mod contract address
|
|
252
|
+
* @param newPrice New price in wei
|
|
253
|
+
* @param details New mod details
|
|
254
|
+
*/
|
|
255
|
+
function update(
|
|
256
|
+
address mod,
|
|
257
|
+
uint256 newPrice,
|
|
258
|
+
ITotemTypes.ModDetails calldata details
|
|
259
|
+
) external nonReentrant {
|
|
260
|
+
ITotemTypes.Mod storage modData = mods[mod];
|
|
261
|
+
|
|
262
|
+
if (modData.mod == address(0)) revert ModNotFound(mod);
|
|
263
|
+
if (msg.sender != modData.seller) revert Unauthorized();
|
|
264
|
+
|
|
265
|
+
_validateModDetails(details);
|
|
266
|
+
|
|
267
|
+
modData.price = newPrice;
|
|
268
|
+
modData.details = details;
|
|
269
|
+
modData.updatedAt = uint64(block.timestamp);
|
|
270
|
+
|
|
271
|
+
emit ModUpdated(mod);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* @notice Update required actions for a mod
|
|
276
|
+
* @param mod Mod contract address
|
|
277
|
+
* @param requiredActions New required setup actions
|
|
278
|
+
*/
|
|
279
|
+
function updateRequiredActions(
|
|
280
|
+
address mod,
|
|
281
|
+
ITotemTypes.ModRequiredAction[] calldata requiredActions
|
|
282
|
+
) external {
|
|
283
|
+
ITotemTypes.Mod storage modData = mods[mod];
|
|
284
|
+
|
|
285
|
+
if (modData.mod == address(0)) revert ModNotFound(mod);
|
|
286
|
+
if (msg.sender != modData.seller) revert Unauthorized();
|
|
287
|
+
|
|
288
|
+
// Clear existing required actions
|
|
289
|
+
delete modRequiredActions[mod];
|
|
290
|
+
|
|
291
|
+
// Store new required actions
|
|
292
|
+
for (uint256 i = 0; i < requiredActions.length; i++) {
|
|
293
|
+
modRequiredActions[mod].push(requiredActions[i]);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
modData.updatedAt = uint64(block.timestamp);
|
|
297
|
+
|
|
298
|
+
emit ModUpdated(mod);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @notice Get multiple mods by their addresses
|
|
303
|
+
* @param contracts Array of mod contract addresses
|
|
304
|
+
* @return Array of Mod structs
|
|
305
|
+
*/
|
|
306
|
+
function getMods(address[] calldata contracts) external view returns (ITotemTypes.Mod[] memory) {
|
|
307
|
+
// Count valid mods first
|
|
308
|
+
uint256 validCount = 0;
|
|
309
|
+
for (uint256 i = 0; i < contracts.length; i++) {
|
|
310
|
+
if (mods[contracts[i]].mod != address(0)) {
|
|
311
|
+
validCount++;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Create result array with correct size
|
|
316
|
+
ITotemTypes.Mod[] memory result = new ITotemTypes.Mod[](validCount);
|
|
317
|
+
uint256 resultIndex = 0;
|
|
318
|
+
|
|
319
|
+
for (uint256 i = 0; i < contracts.length; i++) {
|
|
320
|
+
if (mods[contracts[i]].mod != address(0)) {
|
|
321
|
+
result[resultIndex] = mods[contracts[i]];
|
|
322
|
+
resultIndex++;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* @notice List mods with pagination
|
|
331
|
+
* @param perPage Number of results per page
|
|
332
|
+
* @param cursor Index to start from (for pagination)
|
|
333
|
+
* @return mods_ Array of Mod structs
|
|
334
|
+
* @return nextCursor Next cursor value
|
|
335
|
+
* @return hasMore Whether more results exist
|
|
336
|
+
*/
|
|
337
|
+
function listMods(
|
|
338
|
+
uint32 perPage,
|
|
339
|
+
uint256 cursor
|
|
340
|
+
) external view returns (
|
|
341
|
+
ITotemTypes.Mod[] memory mods_,
|
|
342
|
+
uint256 nextCursor,
|
|
343
|
+
bool hasMore
|
|
344
|
+
) {
|
|
345
|
+
uint256 startIndex = cursor;
|
|
346
|
+
uint256 endIndex = startIndex + perPage;
|
|
347
|
+
|
|
348
|
+
if (endIndex > modList.length) {
|
|
349
|
+
endIndex = modList.length;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
uint256 resultCount = endIndex - startIndex;
|
|
353
|
+
mods_ = new ITotemTypes.Mod[](resultCount);
|
|
354
|
+
|
|
355
|
+
for (uint256 i = 0; i < resultCount; i++) {
|
|
356
|
+
mods_[i] = mods[modList[startIndex + i]];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
nextCursor = endIndex;
|
|
360
|
+
hasMore = endIndex < modList.length;
|
|
361
|
+
|
|
362
|
+
return (mods_, nextCursor, hasMore);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* @notice Get a single mod by address
|
|
367
|
+
* @param mod Mod contract address
|
|
368
|
+
* @return Mod struct
|
|
369
|
+
*/
|
|
370
|
+
function getMod(address mod) external view returns (ITotemTypes.Mod memory) {
|
|
371
|
+
if (mods[mod].mod == address(0)) {
|
|
372
|
+
revert ModNotFound(mod);
|
|
373
|
+
}
|
|
374
|
+
return mods[mod];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* @notice Get required actions for a mod (setup actions called after totem creation)
|
|
379
|
+
* @param mod Mod contract address
|
|
380
|
+
* @return Array of required actions
|
|
381
|
+
*/
|
|
382
|
+
function getModRequiredActions(address mod) external view returns (ITotemTypes.ModRequiredAction[] memory) {
|
|
383
|
+
if (mods[mod].mod == address(0)) {
|
|
384
|
+
revert ModNotFound(mod);
|
|
385
|
+
}
|
|
386
|
+
return modRequiredActions[mod];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* @notice Get the usage price for a single mod
|
|
391
|
+
* @param mod Mod contract address
|
|
392
|
+
* @return The price in wei to use this mod
|
|
393
|
+
*/
|
|
394
|
+
function getModFee(address mod) external view returns (uint256) {
|
|
395
|
+
if (mods[mod].mod == address(0)) {
|
|
396
|
+
revert ModNotFound(mod);
|
|
397
|
+
}
|
|
398
|
+
return mods[mod].price;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* @notice Get the total usage price for multiple mods
|
|
403
|
+
* @dev Useful for calculating total cost when creating a totem with multiple mods
|
|
404
|
+
* @param contracts Array of mod contract addresses
|
|
405
|
+
* @return Total price in wei for all mods combined
|
|
406
|
+
*/
|
|
407
|
+
function getModsFee(address[] calldata contracts) external view returns (uint256) {
|
|
408
|
+
uint256 fee = 0;
|
|
409
|
+
for (uint256 i = 0; i < contracts.length; i++) {
|
|
410
|
+
if (mods[contracts[i]].mod == address(0)) {
|
|
411
|
+
revert ModNotFound(contracts[i]);
|
|
412
|
+
}
|
|
413
|
+
fee += mods[contracts[i]].price;
|
|
414
|
+
}
|
|
415
|
+
return fee;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* @notice Get the hooks supported by a mod
|
|
420
|
+
* @dev Hooks determine when the mod is called (Created, Mint, Burn, Transfer, TransferOwnership)
|
|
421
|
+
* @param mod Mod contract address
|
|
422
|
+
* @return Array of Hook enum values
|
|
423
|
+
*/
|
|
424
|
+
function getSupportedHooks(address mod) external view returns (ITotemTypes.Hook[] memory) {
|
|
425
|
+
if (mods[mod].mod == address(0)) {
|
|
426
|
+
revert ModNotFound(mod);
|
|
427
|
+
}
|
|
428
|
+
return mods[mod].hooks;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* @notice Check if a mod requires unlimited minting capability
|
|
433
|
+
* @dev Mods with needsUnlimited=true can mint tokens without supply cap restrictions.
|
|
434
|
+
* Returns false for unpublished mods.
|
|
435
|
+
* @param mod Mod contract address
|
|
436
|
+
* @return True if the mod needs unlimited minting capability
|
|
437
|
+
*/
|
|
438
|
+
function isUnlimitedMinter(address mod) external view returns (bool) {
|
|
439
|
+
if(mods[mod].publishedAt == 0) return false;
|
|
440
|
+
return mods[mod].details.needsUnlimited;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* @notice Validate mod details (name, summary, image)
|
|
445
|
+
* @dev Reverts if any validation fails
|
|
446
|
+
* @param details The mod details to validate
|
|
447
|
+
*/
|
|
448
|
+
function _validateModDetails(ITotemTypes.ModDetails calldata details) internal pure {
|
|
449
|
+
if (bytes(details.name).length == 0) revert EmptyModName();
|
|
450
|
+
if (bytes(details.name).length < 3) revert ModNameTooShort(bytes(details.name).length);
|
|
451
|
+
if (bytes(details.name).length > 100) revert ModNameTooLong(bytes(details.name).length);
|
|
452
|
+
if (bytes(details.summary).length == 0) revert EmptyModSummary();
|
|
453
|
+
if (bytes(details.summary).length < 10) revert ModSummaryTooShort(bytes(details.summary).length);
|
|
454
|
+
if (bytes(details.summary).length > 150) revert ModSummaryTooLong(bytes(details.summary).length);
|
|
455
|
+
if (bytes(details.image).length == 0) revert EmptyModImage();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* @notice Check if a hook is valid
|
|
460
|
+
* @param hook The hook identifier to validate
|
|
461
|
+
* @return bool True if hook is valid
|
|
462
|
+
*/
|
|
463
|
+
function _isValidHook(ITotemTypes.Hook hook) internal pure returns (bool) {
|
|
464
|
+
return uint8(hook) <= uint8(ITotemTypes.Hook.TransferOwnership);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @title ReentrancyGuard
|
|
6
|
+
* @notice Simple reentrancy guard to prevent reentrant calls
|
|
7
|
+
*/
|
|
8
|
+
abstract contract ReentrancyGuard {
|
|
9
|
+
bool private _locked;
|
|
10
|
+
|
|
11
|
+
modifier nonReentrant() {
|
|
12
|
+
require(!_locked, "ReentrancyGuard: reentrant call");
|
|
13
|
+
_locked = true;
|
|
14
|
+
_;
|
|
15
|
+
_locked = false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
import "../library/ITotemTypes.sol";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @title Shared
|
|
8
|
+
* @notice Shared utilities for fee management and token handling
|
|
9
|
+
* @dev Contains common functions used across Totems contracts
|
|
10
|
+
*/
|
|
11
|
+
library Shared {
|
|
12
|
+
|
|
13
|
+
// ==================== ERRORS ====================
|
|
14
|
+
|
|
15
|
+
/// @notice ETH transfer failed
|
|
16
|
+
error TransferFailed();
|
|
17
|
+
|
|
18
|
+
// ==================== FUNCTIONS ====================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @notice Safely transfer ETH to an address
|
|
22
|
+
* @dev Uses low-level call instead of transfer() to support smart contract recipients.
|
|
23
|
+
* transfer() only forwards 2300 gas which fails for contracts with receive() logic.
|
|
24
|
+
* @param to Recipient address
|
|
25
|
+
* @param amount Amount of ETH to send in wei
|
|
26
|
+
*/
|
|
27
|
+
function safeTransfer(address to, uint256 amount) internal {
|
|
28
|
+
(bool success, ) = payable(to).call{value: amount}("");
|
|
29
|
+
if (!success) revert TransferFailed();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @notice Dispense tokens to multiple recipients
|
|
34
|
+
* @param disbursements Array of fee disbursements
|
|
35
|
+
* @dev Transfers native currency to recipients, skips zero-amount disbursements
|
|
36
|
+
*/
|
|
37
|
+
function dispenseTokens(ITotemTypes.FeeDisbursement[] memory disbursements) internal {
|
|
38
|
+
for (uint256 i = 0; i < disbursements.length; i++) {
|
|
39
|
+
ITotemTypes.FeeDisbursement memory disbursement = disbursements[i];
|
|
40
|
+
if (disbursement.amount > 0) {
|
|
41
|
+
safeTransfer(disbursement.recipient, disbursement.amount);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|