@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.
@@ -0,0 +1,835 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ pragma solidity ^0.8.28;
3
+
4
+ import "../interfaces/IMarket.sol";
5
+ import "../interfaces/TotemMod.sol";
6
+ import "../interfaces/IRelayFactory.sol";
7
+ import "../library/ITotemTypes.sol";
8
+ import "../library/TotemsLibrary.sol";
9
+ import "../shared/Shared.sol";
10
+ import "../shared/ReentrancyGuard.sol";
11
+ import "./Errors.sol";
12
+
13
+ /**
14
+ * @title Totems
15
+ * @notice Consolidated contract for totem creation, operations, and views
16
+ * @dev Combines TotemsCore, TotemsOperations, and TotemsView into a single contract
17
+ */
18
+ contract Totems is ReentrancyGuard {
19
+ // ==================== STATE VARIABLES ====================
20
+
21
+ /// @notice Mapping of referrer addresses to their fee amounts
22
+ mapping(address => uint256) internal fees;
23
+
24
+ /// @notice Mapping of totem ticker to mod address to license status
25
+ mapping(bytes32 => mapping(address => bool)) internal licenses;
26
+
27
+ /// @notice Mapping of normalized ticker bytes to totem data
28
+ mapping(bytes32 => ITotemTypes.Totem) internal totems;
29
+
30
+ /// @notice Mapping of normalized ticker bytes to totem statistics
31
+ mapping(bytes32 => ITotemTypes.TotemStats) internal stats;
32
+
33
+ /// @notice Mapping of totem ticker to account address to token balance
34
+ mapping(bytes32 => mapping(address => uint256)) internal balances;
35
+
36
+ /// @notice Array of all totem ticker bytes for enumeration
37
+ bytes32[] public totemList;
38
+
39
+ /// @notice Address of the mod market contract
40
+ address public marketContract;
41
+
42
+ /// @notice Mapping of totem ticker to relay address to authorization status
43
+ mapping(bytes32 => mapping(address => bool)) internal authorizedRelays;
44
+
45
+ /// @notice Mapping of totem ticker to array of authorized relay info
46
+ mapping(bytes32 => ITotemTypes.RelayInfo[]) internal authorizedRelaysList;
47
+
48
+ /// @dev Nonce used for tracking unique mods during creation
49
+ uint256 internal modNonce;
50
+
51
+ /// @dev Mapping to track which mods have been seen at a given nonce
52
+ mapping(address => uint256) internal seenModAt;
53
+
54
+ /// @notice Address of the proxy mod for license delegation
55
+ address public proxyMod;
56
+
57
+ /// @notice Minimum base fee for totem creation (set once in constructor, never changed)
58
+ uint256 public minBaseFee;
59
+
60
+ /// @notice Fee amount that is always burned (set once in constructor, never changed)
61
+ uint256 public burnedFee;
62
+
63
+ // ==================== EVENTS ====================
64
+
65
+ /// @notice Emitted when a new totem is created
66
+ event TotemCreated(string ticker, address indexed creator);
67
+
68
+ /// @notice Emitted when a relay is authorized for a totem
69
+ event RelayAuthorized(string ticker, address indexed relay);
70
+
71
+ /// @notice Emitted when a relay authorization is revoked
72
+ event RelayRevoked(string ticker, address indexed relay);
73
+
74
+ /// @notice Emitted when tokens are minted for a totem
75
+ event TotemMinted(
76
+ string ticker,
77
+ address indexed minter,
78
+ address mod,
79
+ uint256 minted,
80
+ uint256 payment
81
+ );
82
+
83
+ /// @notice Emitted when tokens are burned from a totem
84
+ event TotemBurned(
85
+ string ticker,
86
+ address indexed owner,
87
+ uint256 amount
88
+ );
89
+
90
+ /// @notice Emitted when tokens are transferred between addresses
91
+ event TotemTransferred(
92
+ string ticker,
93
+ address indexed from,
94
+ address indexed to,
95
+ uint256 amount
96
+ );
97
+
98
+ /// @notice Emitted when ownership of a totem is transferred
99
+ event TotemOwnershipTransferred(
100
+ string ticker,
101
+ address indexed previousOwner,
102
+ address indexed newOwner
103
+ );
104
+
105
+ // ==================== CONSTRUCTOR ====================
106
+
107
+ constructor(
108
+ address _marketContract,
109
+ address _proxyMod,
110
+ uint256 _minBaseFee,
111
+ uint256 _burnedFee
112
+ ) {
113
+ require(_marketContract != address(0), "Invalid marketContract");
114
+ require(_proxyMod != address(0), "Invalid proxyMod");
115
+
116
+ marketContract = _marketContract;
117
+ proxyMod = _proxyMod;
118
+ minBaseFee = _minBaseFee;
119
+ burnedFee = _burnedFee;
120
+ }
121
+
122
+ // ==================== CORE FUNCTIONS ====================
123
+
124
+ /// @notice Creates a new totem with the specified details, allocations, and mods
125
+ function create(
126
+ ITotemTypes.TotemDetails calldata details,
127
+ ITotemTypes.MintAllocation[] calldata allocations,
128
+ ITotemTypes.TotemMods calldata mods,
129
+ address payable referrer
130
+ ) external payable nonReentrant {
131
+ IMarket market = IMarket(marketContract);
132
+ bytes32 tickerBytes = TotemsLibrary.tickerToBytes(details.ticker);
133
+
134
+ // Validate inputs
135
+ _validateCreationInputs(details, allocations, mods, tickerBytes);
136
+
137
+ // Initialize totem
138
+ ITotemTypes.Totem storage totem = totems[tickerBytes];
139
+ totem.creator = payable(msg.sender);
140
+ totem.mods = mods;
141
+ totem.details = details;
142
+ totem.createdAt = uint64(block.timestamp);
143
+ totem.updatedAt = uint64(block.timestamp);
144
+ totem.isActive = false;
145
+
146
+ // Initialize stats
147
+ stats[tickerBytes] = ITotemTypes.TotemStats({
148
+ mints: 0,
149
+ burns: 0,
150
+ transfers: 0,
151
+ holders: 0
152
+ });
153
+
154
+ // Validate mods
155
+ _validateModsForHook(market, mods.transfer, ITotemTypes.Hook.Transfer);
156
+ _validateModsForHook(market, mods.mint, ITotemTypes.Hook.Mint);
157
+ _validateModsForHook(market, mods.burn, ITotemTypes.Hook.Burn);
158
+ _validateModsForHook(market, mods.created, ITotemTypes.Hook.Created);
159
+ _validateModsForHook(market, mods.transferOwnership, ITotemTypes.Hook.TransferOwnership);
160
+
161
+ (ITotemTypes.FeeDisbursement[] memory disbursements, uint256 totalFee) =
162
+ _processFeesAndMods(market, tickerBytes, mods, referrer);
163
+
164
+ if (msg.value < totalFee) {
165
+ revert Errors.InsufficientFee(totalFee, msg.value);
166
+ }
167
+
168
+ // Process allocations
169
+ (uint256 maxSupply, bool hasUnlimitedMinters) = _processAllocations(
170
+ market,
171
+ tickerBytes,
172
+ allocations,
173
+ totem
174
+ );
175
+
176
+ if (maxSupply == 0 && !hasUnlimitedMinters) {
177
+ revert Errors.ZeroSupply();
178
+ }
179
+
180
+ // Finalize totem
181
+ totem.supply = maxSupply;
182
+ totem.maxSupply = maxSupply;
183
+ totemList.push(tickerBytes);
184
+
185
+ // Distribute fees
186
+ Shared.dispenseTokens(disbursements);
187
+
188
+ emit TotemCreated(details.ticker, msg.sender);
189
+
190
+ _notifyCreatedHooks(details.ticker, msg.sender, mods.created);
191
+
192
+ if (msg.value > totalFee) {
193
+ Shared.safeTransfer(msg.sender, msg.value - totalFee);
194
+ }
195
+
196
+ totems[tickerBytes].isActive = true;
197
+ }
198
+
199
+ /// @notice Sets the referrer fee for the caller
200
+ function setReferrerFee(uint256 fee) external {
201
+ if (fee < minBaseFee) {
202
+ revert Errors.ReferrerFeeTooLow(minBaseFee);
203
+ }
204
+ fees[msg.sender] = fee;
205
+ }
206
+
207
+ /// @notice Creates a new relay for a totem using a relay factory
208
+ function createRelay(string calldata ticker, address relayFactory, string calldata standard) external returns (address relay) {
209
+ bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
210
+ if (totems[tickerBytes].creator != msg.sender) revert Errors.Unauthorized();
211
+
212
+ IRelayFactory factory = IRelayFactory(relayFactory);
213
+ relay = factory.createRelay(ticker);
214
+
215
+ authorizedRelays[tickerBytes][relay] = true;
216
+ authorizedRelaysList[tickerBytes].push(
217
+ ITotemTypes.RelayInfo({
218
+ standard: standard,
219
+ relay: relay
220
+ })
221
+ );
222
+ emit RelayAuthorized(ticker, relay);
223
+ }
224
+
225
+ /// @notice Authorizes an existing relay address for a totem
226
+ function addRelay(string calldata ticker, address relay, string calldata standard) external {
227
+ bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
228
+ if (totems[tickerBytes].creator != msg.sender) revert Errors.Unauthorized();
229
+ authorizedRelays[tickerBytes][relay] = true;
230
+ authorizedRelaysList[tickerBytes].push(
231
+ ITotemTypes.RelayInfo({
232
+ standard: standard,
233
+ relay: relay
234
+ })
235
+ );
236
+ emit RelayAuthorized(ticker, relay);
237
+ }
238
+
239
+ /// @notice Revokes authorization for a relay from a totem
240
+ function removeRelay(string calldata ticker, address relay) external {
241
+ bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
242
+ if (totems[tickerBytes].creator != msg.sender) revert Errors.Unauthorized();
243
+ authorizedRelays[tickerBytes][relay] = false;
244
+
245
+ ITotemTypes.RelayInfo[] storage relays = authorizedRelaysList[tickerBytes];
246
+ uint256 length = relays.length;
247
+ for (uint256 i = 0; i < length; i++) {
248
+ if (relays[i].relay == relay) {
249
+ relays[i] = relays[length - 1];
250
+ relays.pop();
251
+ break;
252
+ }
253
+ }
254
+
255
+ emit RelayRevoked(ticker, relay);
256
+ }
257
+
258
+ /// @notice Grants a mod license for a totem, callable only by the proxy mod
259
+ function setLicenseFromProxy(bytes32 tickerBytes, address mod) external {
260
+ if (msg.sender != proxyMod) {
261
+ revert Errors.Unauthorized();
262
+ }
263
+ if (totems[tickerBytes].creator == address(0)) {
264
+ revert Errors.CantSetLicense();
265
+ }
266
+
267
+ _storeLicense(tickerBytes, mod);
268
+ }
269
+
270
+ // ==================== OPERATIONS FUNCTIONS ====================
271
+
272
+ /// @notice Mints tokens for a totem using an authorized minter mod
273
+ function mint(
274
+ address mod,
275
+ address minter,
276
+ string calldata ticker,
277
+ uint256 amount,
278
+ string calldata memo
279
+ ) external payable {
280
+ bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
281
+ if (minter != msg.sender && !authorizedRelays[tickerBytes][msg.sender]) {
282
+ revert Errors.Unauthorized();
283
+ }
284
+
285
+ if (totems[tickerBytes].creator == address(0)) {
286
+ revert Errors.TotemNotFound(ticker);
287
+ }
288
+ if (!totems[tickerBytes].isActive) {
289
+ revert Errors.TotemNotActive();
290
+ }
291
+
292
+ // Verify mod is authorized minter
293
+ bool isMinter = false;
294
+ for (uint256 i = 0; i < totems[tickerBytes].allocations.length; i++) {
295
+ if (totems[tickerBytes].allocations[i].recipient == mod && totems[tickerBytes].allocations[i].isMinter) {
296
+ isMinter = true;
297
+ break;
298
+ }
299
+ }
300
+ if (!isMinter) revert Errors.ModNotMinter(mod);
301
+
302
+ // Update balances and stats
303
+ if (balances[tickerBytes][minter] == 0) {
304
+ unchecked {
305
+ stats[tickerBytes].holders++;
306
+ }
307
+ }
308
+
309
+ unchecked {
310
+ stats[tickerBytes].mints++;
311
+ }
312
+
313
+ // Track minter's balance before to measure actual minted amount
314
+ uint256 balanceBefore = balances[tickerBytes][minter];
315
+
316
+ IModMinter(mod).mint{value: msg.value}(ticker, minter, amount, memo);
317
+
318
+ // Measure actual minted amount from balance delta
319
+ uint256 minted = balances[tickerBytes][minter] - balanceBefore;
320
+
321
+ // Notify hooks
322
+ _notifyMintHooks(ticker, minter, amount, msg.value, memo, totems[tickerBytes].mods.mint);
323
+
324
+ emit TotemMinted(ticker, minter, mod, minted, msg.value);
325
+ }
326
+
327
+ /// @notice Burns tokens from a totem, permanently reducing supply
328
+ function burn(
329
+ string calldata ticker,
330
+ address owner,
331
+ uint256 amount,
332
+ string calldata memo
333
+ ) external {
334
+ bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
335
+ if (owner != msg.sender && !authorizedRelays[tickerBytes][msg.sender]) {
336
+ revert Errors.Unauthorized();
337
+ }
338
+
339
+ if (totems[tickerBytes].creator == address(0)) {
340
+ revert Errors.TotemNotFound(ticker);
341
+ }
342
+ if (!totems[tickerBytes].isActive) {
343
+ revert Errors.TotemNotActive();
344
+ }
345
+
346
+ // Burn tokens (will revert if insufficient balance)
347
+ _subBalance(tickerBytes, owner, amount);
348
+
349
+ // Update stats
350
+ if (balances[tickerBytes][owner] == 0) {
351
+ unchecked {
352
+ stats[tickerBytes].holders--;
353
+ }
354
+ }
355
+ totems[tickerBytes].supply -= amount;
356
+ totems[tickerBytes].maxSupply -= amount;
357
+ unchecked {
358
+ stats[tickerBytes].burns++;
359
+ }
360
+
361
+ // Notify hooks
362
+ _notifyBurnHooks(ticker, owner, amount, memo, totems[tickerBytes].mods.burn);
363
+
364
+ emit TotemBurned(ticker, owner, amount);
365
+ }
366
+
367
+ /// @notice Transfers tokens between addresses
368
+ function transfer(
369
+ string calldata ticker,
370
+ address from,
371
+ address to,
372
+ uint256 amount,
373
+ string calldata memo
374
+ ) external {
375
+ bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
376
+ if (from != msg.sender && !authorizedRelays[tickerBytes][msg.sender]) {
377
+ revert Errors.Unauthorized();
378
+ }
379
+
380
+ IMarket market = IMarket(marketContract);
381
+ bool fromIsUnlimited = market.isUnlimitedMinter(from);
382
+
383
+ // Unlimited minters must always have 0 balance
384
+ if (market.isUnlimitedMinter(to)) {
385
+ revert Errors.CannotTransferToUnlimitedMinter();
386
+ }
387
+
388
+ if (totems[tickerBytes].creator == address(0)) {
389
+ revert Errors.TotemNotFound(ticker);
390
+ }
391
+ if (!totems[tickerBytes].isActive) {
392
+ revert Errors.TotemNotActive();
393
+ }
394
+
395
+ // Update balances
396
+ if (!fromIsUnlimited) {
397
+ if (from != to) {
398
+ _subBalance(tickerBytes, from, amount);
399
+ if (balances[tickerBytes][from] == 0) {
400
+ unchecked {
401
+ stats[tickerBytes].holders--;
402
+ }
403
+ }
404
+ }
405
+ }
406
+
407
+ if (from != to) {
408
+ // Track new holder
409
+ if (balances[tickerBytes][to] == 0) {
410
+ unchecked {
411
+ stats[tickerBytes].holders++;
412
+ }
413
+ }
414
+ _addBalance(tickerBytes, to, amount);
415
+ }
416
+
417
+ // Update supply when unlimited minter transfers (minting new tokens)
418
+ if (fromIsUnlimited) {
419
+ totems[tickerBytes].supply += amount;
420
+ totems[tickerBytes].maxSupply += amount;
421
+ }
422
+
423
+ unchecked {
424
+ stats[tickerBytes].transfers++;
425
+ }
426
+
427
+ // Notify hooks
428
+ _notifyTransferHooks(ticker, from, to, amount, memo, totems[tickerBytes].mods.transfer);
429
+
430
+ emit TotemTransferred(ticker, from, to, amount);
431
+ }
432
+
433
+ /// @notice Transfers ownership of a totem to a new address
434
+ function transferOwnership(string calldata ticker, address payable newOwner) external {
435
+ bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
436
+
437
+ if (totems[tickerBytes].creator == address(0)) {
438
+ revert Errors.TotemNotFound(ticker);
439
+ }
440
+
441
+ address previousOwner = totems[tickerBytes].creator;
442
+ if (msg.sender != previousOwner) {
443
+ revert Errors.Unauthorized();
444
+ }
445
+
446
+ require(newOwner != address(0), "New owner cannot be zero address");
447
+
448
+ totems[tickerBytes].creator = newOwner;
449
+
450
+ // Notify hooks
451
+ _notifyTransferOwnershipHooks(ticker, previousOwner, newOwner, totems[tickerBytes].mods.transferOwnership);
452
+
453
+ emit TotemOwnershipTransferred(ticker, previousOwner, newOwner);
454
+ }
455
+
456
+ // ==================== VIEW FUNCTIONS ====================
457
+
458
+ /// @notice Gets the fee for creating a totem
459
+ function getFee(address referrer) external view returns (uint256) {
460
+ if (referrer == address(0)) {
461
+ return minBaseFee;
462
+ }
463
+ uint256 referrerFee = fees[referrer];
464
+ return referrerFee > minBaseFee ? referrerFee : minBaseFee;
465
+ }
466
+
467
+ /// @notice Converts a ticker string to its normalized bytes32 representation
468
+ function tickerToBytes(string calldata ticker) external pure returns (bytes32) {
469
+ return TotemsLibrary.tickerToBytes(ticker);
470
+ }
471
+
472
+ /// @notice Retrieves a totem by its ticker symbol
473
+ function getTotem(string calldata ticker) external view returns (ITotemTypes.Totem memory) {
474
+ bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
475
+ ITotemTypes.Totem memory totem = totems[tickerBytes];
476
+ if (totem.creator == address(0)) {
477
+ revert Errors.TotemNotFound(ticker);
478
+ }
479
+ return totem;
480
+ }
481
+
482
+ /// @notice Retrieves multiple totems by their ticker symbols
483
+ function getTotems(string[] calldata tickers) external view returns (ITotemTypes.Totem[] memory) {
484
+ ITotemTypes.Totem[] memory results = new ITotemTypes.Totem[](tickers.length);
485
+ for (uint256 i = 0; i < tickers.length; i++) {
486
+ bytes32 tickerBytes = TotemsLibrary.tickerToBytes(tickers[i]);
487
+ ITotemTypes.Totem memory totem = totems[tickerBytes];
488
+ if (totem.creator == address(0)) {
489
+ revert Errors.TotemNotFound(tickers[i]);
490
+ }
491
+ results[i] = totem;
492
+ }
493
+ return results;
494
+ }
495
+
496
+ /// @notice Gets the token balance for an account on a specific totem
497
+ function getBalance(string calldata ticker, address account) external view returns (uint256) {
498
+ bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
499
+ return balances[tickerBytes][account];
500
+ }
501
+
502
+ /// @notice Gets the statistics for a totem
503
+ function getStats(string calldata ticker) external view returns (ITotemTypes.TotemStats memory) {
504
+ bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
505
+ return stats[tickerBytes];
506
+ }
507
+
508
+ /// @notice Checks if a mod is licensed for a specific totem
509
+ function isLicensed(string calldata ticker, address mod) external view returns (bool) {
510
+ bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
511
+ return licenses[tickerBytes][mod];
512
+ }
513
+
514
+ /// @notice Gets the proxy mod address
515
+ function getProxyMod() external view returns (address) {
516
+ return proxyMod;
517
+ }
518
+
519
+ /// @notice Lists totems with pagination support
520
+ function listTotems(
521
+ uint32 perPage,
522
+ uint256 cursor
523
+ ) external view returns (ITotemTypes.Totem[] memory, uint256, bool) {
524
+ uint256 length = totemList.length;
525
+
526
+ if (cursor >= length) {
527
+ revert Errors.InvalidCursor();
528
+ }
529
+
530
+ uint256 startIndex = cursor;
531
+ uint256 endIndex = startIndex + perPage;
532
+
533
+ if (endIndex > totemList.length) {
534
+ endIndex = totemList.length;
535
+ }
536
+
537
+ uint256 resultCount = endIndex - startIndex;
538
+ ITotemTypes.Totem[] memory totemResults = new ITotemTypes.Totem[](resultCount);
539
+
540
+ for (uint256 i = 0; i < resultCount; i++) {
541
+ bytes32 tickerBytes = totemList[startIndex + i];
542
+ totemResults[i] = totems[tickerBytes];
543
+ }
544
+
545
+ return (totemResults, endIndex, endIndex < totemList.length);
546
+ }
547
+
548
+ /// @notice Gets all authorized relays for a totem
549
+ function getRelays(string calldata ticker) external view returns (ITotemTypes.RelayInfo[] memory) {
550
+ bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
551
+ return authorizedRelaysList[tickerBytes];
552
+ }
553
+
554
+ /// @notice Gets the relay address for a specific standard on a totem
555
+ function getRelayOfStandard(string calldata ticker, string calldata standard) external view returns (address) {
556
+ bytes32 tickerBytes = TotemsLibrary.tickerToBytes(ticker);
557
+ ITotemTypes.RelayInfo[] storage relays = authorizedRelaysList[tickerBytes];
558
+ uint256 length = relays.length;
559
+ for (uint256 i = 0; i < length; i++) {
560
+ if (keccak256(bytes(relays[i].standard)) == keccak256(bytes(standard))) {
561
+ return relays[i].relay;
562
+ }
563
+ }
564
+ return address(0);
565
+ }
566
+
567
+ // ==================== INTERNAL FUNCTIONS ====================
568
+
569
+ function _validateCreationInputs(
570
+ ITotemTypes.TotemDetails calldata details,
571
+ ITotemTypes.MintAllocation[] calldata allocations,
572
+ ITotemTypes.TotemMods calldata mods,
573
+ bytes32 tickerBytes
574
+ ) internal view {
575
+ if (allocations.length > 50) {
576
+ revert Errors.TooManyAllocations();
577
+ }
578
+
579
+ if (mods.transfer.length + mods.mint.length + mods.burn.length + mods.created.length + mods.transferOwnership.length > 200) {
580
+ revert Errors.TooManyMods();
581
+ }
582
+
583
+ if (totems[tickerBytes].creator != address(0)) {
584
+ revert Errors.TotemAlreadyExists(details.ticker);
585
+ }
586
+
587
+ if (bytes(details.name).length > 32) revert Errors.NameTooLong(bytes(details.name).length);
588
+ if (bytes(details.name).length < 3) revert Errors.NameTooShort(bytes(details.name).length);
589
+ if (bytes(details.image).length == 0) revert Errors.EmptyImage();
590
+ if (bytes(details.description).length > 500) revert Errors.DescriptionTooLong(bytes(details.description).length);
591
+ if (details.seed == bytes32(0)) revert Errors.InvalidSeed();
592
+ }
593
+
594
+ function _processAllocations(
595
+ IMarket market,
596
+ bytes32 tickerBytes,
597
+ ITotemTypes.MintAllocation[] calldata allocations,
598
+ ITotemTypes.Totem storage totem
599
+ ) internal returns (uint256, bool) {
600
+ uint256 maxSupply = 0;
601
+ bool hasUnlimitedMinters = false;
602
+
603
+ for (uint256 i = 0; i < allocations.length; i++) {
604
+ totem.allocations.push(allocations[i]);
605
+
606
+ if (allocations[i].recipient == address(0)) {
607
+ revert Errors.InvalidAllocation("Cannot allocate to zero address");
608
+ }
609
+
610
+ if (allocations[i].isMinter) {
611
+ ITotemTypes.Mod memory mod = market.getMod(allocations[i].recipient);
612
+ if (!mod.details.isMinter) {
613
+ revert Errors.ModNotMinter(allocations[i].recipient);
614
+ }
615
+ if (allocations[i].amount == 0) {
616
+ if (!mod.details.needsUnlimited) {
617
+ revert Errors.ModMustSupportUnlimitedMinting(allocations[i].recipient);
618
+ }
619
+
620
+ if (!hasUnlimitedMinters) hasUnlimitedMinters = true;
621
+ }
622
+ } else {
623
+ if (balances[tickerBytes][allocations[i].recipient] == 0) {
624
+ stats[tickerBytes].holders++;
625
+ stats[tickerBytes].mints++;
626
+ }
627
+
628
+ if (allocations[i].amount == 0) {
629
+ revert Errors.InvalidAllocation("Cannot allocate zero amount to non-minter");
630
+ }
631
+ }
632
+
633
+ if (allocations[i].amount > 0) {
634
+ balances[tickerBytes][allocations[i].recipient] += allocations[i].amount;
635
+ maxSupply += allocations[i].amount;
636
+ }
637
+ }
638
+
639
+ return (maxSupply, hasUnlimitedMinters);
640
+ }
641
+
642
+ function _processFeesAndMods(
643
+ IMarket market,
644
+ bytes32 tickerBytes,
645
+ ITotemTypes.TotemMods calldata mods,
646
+ address payable referrer
647
+ ) internal returns (ITotemTypes.FeeDisbursement[] memory, uint256) {
648
+ uint256 totalLength =
649
+ mods.transfer.length +
650
+ mods.mint.length +
651
+ mods.burn.length +
652
+ mods.created.length +
653
+ mods.transferOwnership.length;
654
+
655
+ modNonce++;
656
+
657
+ address[] memory unique = new address[](totalLength);
658
+ uint256 uniqueCount = 0;
659
+ uint256 totalFees = 0;
660
+
661
+ uniqueCount = _processModArray(mods.transfer, unique, uniqueCount);
662
+ uniqueCount = _processModArray(mods.mint, unique, uniqueCount);
663
+ uniqueCount = _processModArray(mods.burn, unique, uniqueCount);
664
+ uniqueCount = _processModArray(mods.created, unique, uniqueCount);
665
+ uniqueCount = _processModArray(mods.transferOwnership, unique, uniqueCount);
666
+
667
+ // +2 for burn disbursement and optional referrer disbursement
668
+ ITotemTypes.FeeDisbursement[] memory disbursements =
669
+ new ITotemTypes.FeeDisbursement[](uniqueCount + 2);
670
+
671
+ for (uint256 i = 0; i < uniqueCount; i++) {
672
+ address mod = unique[i];
673
+ uint256 fee = market.getModFee(mod);
674
+
675
+ if (fee > 0) {
676
+ disbursements[i] = ITotemTypes.FeeDisbursement({
677
+ recipient: IMod(mod).getSeller(),
678
+ amount: fee
679
+ });
680
+
681
+ totalFees += fee;
682
+ }
683
+ _storeLicense(tickerBytes, mod);
684
+ }
685
+
686
+ // Calculate the base fee (at least minBaseFee, or referrer's set fee if higher)
687
+ uint256 referrerSetFee = referrer != address(0) ? fees[referrer] : 0;
688
+ uint256 baseFee = referrerSetFee > minBaseFee ? referrerSetFee : minBaseFee;
689
+ totalFees += baseFee;
690
+
691
+ if (referrer == address(0)) {
692
+ // No referrer - burn the entire baseFee
693
+ disbursements[uniqueCount] = ITotemTypes.FeeDisbursement({
694
+ recipient: payable(address(0)),
695
+ amount: baseFee
696
+ });
697
+ } else {
698
+ // burnedFee is always burned
699
+ if (burnedFee > 0) {
700
+ disbursements[uniqueCount] = ITotemTypes.FeeDisbursement({
701
+ recipient: payable(address(0)),
702
+ amount: burnedFee
703
+ });
704
+ }
705
+ // Referrer gets the difference between baseFee and burnedFee
706
+ uint256 referrerAmount = baseFee - burnedFee;
707
+ if (referrerAmount > 0) {
708
+ disbursements[uniqueCount + 1] = ITotemTypes.FeeDisbursement({
709
+ recipient: referrer,
710
+ amount: referrerAmount
711
+ });
712
+ }
713
+ }
714
+
715
+ return (disbursements, totalFees);
716
+ }
717
+
718
+ function _processModArray(
719
+ address[] calldata modArray,
720
+ address[] memory unique,
721
+ uint256 uniqueCount
722
+ ) internal returns (uint256) {
723
+ for (uint256 i = 0; i < modArray.length; i++) {
724
+ address mod = modArray[i];
725
+ if (seenModAt[mod] != modNonce) {
726
+ seenModAt[mod] = modNonce;
727
+ unique[uniqueCount++] = mod;
728
+ }
729
+ }
730
+ return uniqueCount;
731
+ }
732
+
733
+ function _storeLicense(bytes32 ticker, address mod) internal {
734
+ licenses[ticker][mod] = true;
735
+ }
736
+
737
+ function _validateModsForHook(
738
+ IMarket market,
739
+ address[] calldata mods,
740
+ ITotemTypes.Hook hook
741
+ ) internal view {
742
+ for (uint256 i = 0; i < mods.length; i++) {
743
+ address mod = mods[i];
744
+ ITotemTypes.Hook[] memory supported = market.getSupportedHooks(mod);
745
+ if (!_supportsHook(supported, hook)) {
746
+ revert Errors.ModDoesntSupportHook(mod, hook);
747
+ }
748
+ }
749
+ }
750
+
751
+ function _supportsHook(
752
+ ITotemTypes.Hook[] memory supported,
753
+ ITotemTypes.Hook required
754
+ ) internal pure returns (bool) {
755
+ for (uint256 i = 0; i < supported.length; i++) {
756
+ if (supported[i] == required) {
757
+ return true;
758
+ }
759
+ }
760
+ return false;
761
+ }
762
+
763
+ function _notifyCreatedHooks(
764
+ string calldata ticker,
765
+ address creator,
766
+ address[] memory mods
767
+ ) internal {
768
+ for (uint256 i = 0; i < mods.length; i++) {
769
+ if (mods[i] == proxyMod) {
770
+ continue;
771
+ }
772
+ IModCreated(mods[i]).onCreated(ticker, creator);
773
+ }
774
+ }
775
+
776
+ function _addBalance(bytes32 ticker, address account, uint256 amount) internal {
777
+ balances[ticker][account] += amount;
778
+ }
779
+
780
+ function _subBalance(bytes32 ticker, address account, uint256 amount) internal {
781
+ if (balances[ticker][account] < amount) {
782
+ revert Errors.InsufficientBalance(amount, balances[ticker][account]);
783
+ }
784
+ balances[ticker][account] -= amount;
785
+ }
786
+
787
+ function _notifyMintHooks(
788
+ string calldata ticker,
789
+ address minter,
790
+ uint256 amount,
791
+ uint256 payment,
792
+ string memory memo,
793
+ address[] memory mods
794
+ ) internal {
795
+ for (uint256 i = 0; i < mods.length; i++) {
796
+ IModMint(mods[i]).onMint(ticker, minter, amount, payment, memo);
797
+ }
798
+ }
799
+
800
+ function _notifyTransferHooks(
801
+ string calldata ticker,
802
+ address from,
803
+ address to,
804
+ uint256 amount,
805
+ string memory memo,
806
+ address[] memory mods
807
+ ) internal {
808
+ for (uint256 i = 0; i < mods.length; i++) {
809
+ IModTransfer(mods[i]).onTransfer(ticker, from, to, amount, memo);
810
+ }
811
+ }
812
+
813
+ function _notifyBurnHooks(
814
+ string calldata ticker,
815
+ address owner,
816
+ uint256 amount,
817
+ string memory memo,
818
+ address[] memory mods
819
+ ) internal {
820
+ for (uint256 i = 0; i < mods.length; i++) {
821
+ IModBurn(mods[i]).onBurn(ticker, owner, amount, memo);
822
+ }
823
+ }
824
+
825
+ function _notifyTransferOwnershipHooks(
826
+ string calldata ticker,
827
+ address previousOwner,
828
+ address newOwner,
829
+ address[] memory mods
830
+ ) internal {
831
+ for (uint256 i = 0; i < mods.length; i++) {
832
+ IModTransferOwnership(mods[i]).onTransferOwnership(ticker, previousOwner, newOwner);
833
+ }
834
+ }
835
+ }