@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,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
|
+
}
|