@dpa-oss/dpa 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,740 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.20;
3
+
4
+ import "erc721a/contracts/ERC721A.sol";
5
+ import "@openzeppelin/contracts/access/Ownable.sol";
6
+ import "@openzeppelin/contracts/utils/Pausable.sol";
7
+ import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
8
+ import "./shared/Types.sol";
9
+ import "./shared/Errors.sol";
10
+ import "./shared/IDPA.sol";
11
+ import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
12
+
13
+ /**
14
+ * @title DPA (Digital Public Asset)
15
+ * @dev Abstract contract for creating digital public assets with ERC721A
16
+ * Features:
17
+ * - Orchestrator-only minting (changeable by owner)
18
+ * - Token content storage as generic bytes (decoded by implementers)
19
+ * - Batch minting support
20
+ * - Protocol-enforced revision linked-list for full traceability
21
+ * - On-chain events for all operations
22
+ */
23
+ abstract contract DPA is ERC721A, Ownable, Pausable, ReentrancyGuard {
24
+ // ============ State Variables ============
25
+
26
+ /// @notice The orchestrator address (only this address can mint/revise)
27
+ address public orchestrator;
28
+
29
+ /// @dev Token ID -> Token URI
30
+ mapping(uint256 => string) private _tokenURIs;
31
+
32
+ /// @dev Token ID -> Token content (encoded bytes, decoded by implementers)
33
+ mapping(uint256 => bytes) private _tokenContents;
34
+
35
+ /// @dev Token ID -> Revision record
36
+ mapping(uint256 => Types.RevisionRecord) private _revisions;
37
+
38
+ /// @dev Token ID -> Child token ID (single child for linear chain)
39
+ mapping(uint256 => uint256) private _childToken;
40
+
41
+ /// @dev Token ID -> name hash -> linked DPA contract address
42
+ mapping(uint256 => mapping(bytes32 => address)) private _linkedDPAs;
43
+
44
+ /// @dev Token ID -> array of link name hashes (for enumeration)
45
+ mapping(uint256 => bytes32[]) private _linkedDPANames;
46
+
47
+ /// @dev Counter for total unique digital public assets (origin tokens only)
48
+ uint256 private _totalAssets;
49
+
50
+ // ============ Events ============
51
+
52
+ /// @notice Emitted when a new token is minted
53
+ event TokenMinted(uint256 indexed tokenId, address indexed to, string uri);
54
+
55
+ /// @notice Emitted when tokens are batch minted
56
+ event BatchMinted(
57
+ uint256 indexed startTokenId,
58
+ uint256 quantity,
59
+ address indexed to
60
+ );
61
+
62
+ /// @notice Emitted when a token is revised (full reason string for off-chain indexing)
63
+ event TokenRevised(
64
+ uint256 indexed newTokenId,
65
+ uint256 indexed parentTokenId,
66
+ uint256 indexed originTokenId,
67
+ string reason
68
+ );
69
+
70
+ /// @notice Emitted when orchestrator is changed
71
+ event OrchestratorUpdated(
72
+ address indexed previousOrchestrator,
73
+ address indexed newOrchestrator
74
+ );
75
+
76
+ /// @notice Emitted when a DPA contract is linked to a token
77
+ event DPALinked(
78
+ uint256 indexed tokenId,
79
+ bytes32 indexed nameHash,
80
+ address indexed dpaContract,
81
+ string name
82
+ );
83
+
84
+ /// @notice Emitted when a DPA contract is unlinked from a token
85
+ event DPAUnlinked(
86
+ uint256 indexed tokenId,
87
+ bytes32 indexed nameHash,
88
+ address indexed dpaContract
89
+ );
90
+
91
+ // ============ Modifiers ============
92
+
93
+ /// @dev Restricts function access to orchestrator only
94
+ modifier onlyOrchestrator() {
95
+ if (msg.sender != orchestrator) {
96
+ revert Errors.NotOrchestrator();
97
+ }
98
+ _;
99
+ }
100
+
101
+ /// @dev Restricts function to only operate on latest version tokens
102
+ modifier onlyLatestVersion(uint256 tokenId) {
103
+ if (!_exists(tokenId)) {
104
+ revert Errors.InvalidTokenId();
105
+ }
106
+ if (_childToken[tokenId] != 0) {
107
+ revert Errors.NotLatestVersion();
108
+ }
109
+ _;
110
+ }
111
+
112
+ // ============ Constructor ============
113
+
114
+ /**
115
+ * @param name_ Token collection name
116
+ * @param symbol_ Token collection symbol
117
+ * @param orchestrator_ Initial orchestrator address
118
+ */
119
+ constructor(
120
+ string memory name_,
121
+ string memory symbol_,
122
+ address orchestrator_
123
+ ) ERC721A(name_, symbol_) Ownable(msg.sender) {
124
+ if (orchestrator_ == address(0)) {
125
+ revert Errors.ZeroAddress();
126
+ }
127
+ orchestrator = orchestrator_;
128
+ }
129
+
130
+ // ============ Admin Functions ============
131
+
132
+ /**
133
+ * @notice Updates the orchestrator address
134
+ * @param newOrchestrator New orchestrator address
135
+ */
136
+ function setOrchestrator(address newOrchestrator) external onlyOwner {
137
+ if (newOrchestrator == address(0)) {
138
+ revert Errors.ZeroAddress();
139
+ }
140
+ address previous = orchestrator;
141
+ orchestrator = newOrchestrator;
142
+ emit OrchestratorUpdated(previous, newOrchestrator);
143
+ }
144
+
145
+ /**
146
+ * @notice Pauses all minting and revision operations
147
+ */
148
+ function pause() external onlyOwner {
149
+ _pause();
150
+ }
151
+
152
+ /**
153
+ * @notice Unpauses all minting and revision operations
154
+ */
155
+ function unpause() external onlyOwner {
156
+ _unpause();
157
+ }
158
+
159
+ // ============ Minting Functions ============
160
+
161
+ /**
162
+ * @notice Mints a single token with URI and content
163
+ * @param to Recipient address
164
+ * @param uri Token URI (metadata location)
165
+ * @param content Encoded content data (decoded by derived contract)
166
+ * @return tokenId The minted token ID
167
+ */
168
+ function mint(
169
+ address to,
170
+ string calldata uri,
171
+ bytes calldata content
172
+ )
173
+ external
174
+ onlyOrchestrator
175
+ whenNotPaused
176
+ nonReentrant
177
+ returns (uint256 tokenId)
178
+ {
179
+ tokenId = _nextTokenId();
180
+
181
+ // Validate content through derived contract
182
+ _validateContent(content);
183
+
184
+ // Mint the token
185
+ _mint(to, 1);
186
+
187
+ // Store URI and content
188
+ _tokenURIs[tokenId] = uri;
189
+ _tokenContents[tokenId] = content;
190
+
191
+ // Create initial revision record (no parent, version 1)
192
+ _revisions[tokenId] = Types.RevisionRecord({
193
+ previousTokenId: 0,
194
+ originTokenId: tokenId, // Self-reference for origin
195
+ version: 1, // Origin is version 1
196
+ reasonHash: bytes32(0), // No reason for initial creation
197
+ timestamp: block.timestamp,
198
+ actor: msg.sender
199
+ });
200
+
201
+ // Increment total unique assets counter
202
+ _totalAssets++;
203
+
204
+ emit TokenMinted(tokenId, to, uri);
205
+ }
206
+
207
+ /**
208
+ * @notice Mints a single token with explicit owner specification
209
+ * @param to Recipient address
210
+ * @param owner_ Token owner for record
211
+ * @param uri Token URI
212
+ * @param content Encoded content data
213
+ * @return tokenId The minted token ID
214
+ */
215
+ function mintWithOwner(
216
+ address to,
217
+ address owner_,
218
+ string calldata uri,
219
+ bytes calldata content
220
+ )
221
+ external
222
+ onlyOrchestrator
223
+ whenNotPaused
224
+ nonReentrant
225
+ returns (uint256 tokenId)
226
+ {
227
+ tokenId = _nextTokenId();
228
+
229
+ _validateContent(content);
230
+ _mint(to, 1);
231
+
232
+ _tokenURIs[tokenId] = uri;
233
+ _tokenContents[tokenId] = content;
234
+
235
+ _revisions[tokenId] = Types.RevisionRecord({
236
+ previousTokenId: 0,
237
+ originTokenId: tokenId,
238
+ version: 1, // Origin is version 1
239
+ reasonHash: bytes32(0),
240
+ timestamp: block.timestamp,
241
+ actor: owner_ // Use explicit owner as actor
242
+ });
243
+
244
+ // Increment total unique assets counter
245
+ _totalAssets++;
246
+
247
+ emit TokenMinted(tokenId, to, uri);
248
+ }
249
+
250
+ /**
251
+ * @notice Batch mints tokens with URIs and contents
252
+ * @param to Recipient address (same for all tokens in batch)
253
+ * @param uris Array of token URIs
254
+ * @param contents Array of encoded content data
255
+ * @return startTokenId The first token ID in the batch
256
+ */
257
+ function batchMint(
258
+ address to,
259
+ string[] calldata uris,
260
+ bytes[] calldata contents
261
+ )
262
+ external
263
+ onlyOrchestrator
264
+ whenNotPaused
265
+ nonReentrant
266
+ returns (uint256 startTokenId)
267
+ {
268
+ uint256 quantity = uris.length;
269
+
270
+ if (quantity != contents.length) {
271
+ revert Errors.ArrayLengthMismatch();
272
+ }
273
+ if (quantity == 0) {
274
+ revert Errors.BatchMintFailed();
275
+ }
276
+
277
+ startTokenId = _nextTokenId();
278
+
279
+ // Validate all content first
280
+ for (uint256 i = 0; i < quantity; i++) {
281
+ _validateContent(contents[i]);
282
+ }
283
+
284
+ // Batch mint all tokens
285
+ _mint(to, quantity);
286
+
287
+ // Store URIs, contents, and create revision records
288
+ for (uint256 i = 0; i < quantity; i++) {
289
+ uint256 tokenId = startTokenId + i;
290
+ _tokenURIs[tokenId] = uris[i];
291
+ _tokenContents[tokenId] = contents[i];
292
+
293
+ _revisions[tokenId] = Types.RevisionRecord({
294
+ previousTokenId: 0,
295
+ originTokenId: tokenId,
296
+ version: 1, // Origin is version 1
297
+ reasonHash: bytes32(0),
298
+ timestamp: block.timestamp,
299
+ actor: msg.sender
300
+ });
301
+
302
+ emit TokenMinted(tokenId, to, uris[i]);
303
+ }
304
+
305
+ // Increment total unique assets counter by batch size
306
+ _totalAssets += quantity;
307
+
308
+ emit BatchMinted(startTokenId, quantity, to);
309
+ }
310
+
311
+ // ============ Revision Functions ============
312
+
313
+ /**
314
+ * @notice Creates a new revision of an existing token (protocol-enforced)
315
+ * @param parentTokenId The token being revised (must be latest version)
316
+ * @param uri New token URI
317
+ * @param content New encoded content
318
+ * @param reason Human-readable reason (stored as hash, emitted in full)
319
+ * @return newTokenId The newly minted revision token
320
+ */
321
+ function revise(
322
+ uint256 parentTokenId,
323
+ string calldata uri,
324
+ bytes calldata content,
325
+ string calldata reason
326
+ )
327
+ external
328
+ onlyOrchestrator
329
+ whenNotPaused
330
+ nonReentrant
331
+ returns (uint256 newTokenId)
332
+ {
333
+ newTokenId = _revise(parentTokenId, uri, content, reason);
334
+ }
335
+
336
+ /**
337
+ * @dev Internal revision function - NOT virtual (protocol-enforced, cannot be overridden)
338
+ */
339
+ function _revise(
340
+ uint256 parentTokenId,
341
+ string calldata uri,
342
+ bytes calldata content,
343
+ string calldata reason
344
+ ) internal onlyLatestVersion(parentTokenId) returns (uint256 newTokenId) {
345
+ // Note: _exists check is now in onlyLatestVersion modifier
346
+
347
+ _validateContent(content);
348
+
349
+ newTokenId = _nextTokenId();
350
+
351
+ // Mint new revision token to same owner as parent
352
+ address parentOwner = ownerOf(parentTokenId);
353
+ _mint(parentOwner, 1);
354
+
355
+ // Store new token data
356
+ _tokenURIs[newTokenId] = uri;
357
+ _tokenContents[newTokenId] = content;
358
+
359
+ // Find origin token (root of chain)
360
+ uint256 originId = _revisions[parentTokenId].originTokenId;
361
+ if (originId == 0) {
362
+ originId = parentTokenId;
363
+ }
364
+
365
+ // Get parent version and increment
366
+ uint256 newVersion = _revisions[parentTokenId].version + 1;
367
+
368
+ // Create revision record with links (reason stored as hash for gas efficiency)
369
+ _revisions[newTokenId] = Types.RevisionRecord({
370
+ previousTokenId: parentTokenId,
371
+ originTokenId: originId,
372
+ version: newVersion,
373
+ reasonHash: keccak256(bytes(reason)),
374
+ timestamp: block.timestamp,
375
+ actor: msg.sender
376
+ });
377
+
378
+ // Update parent's child (single child for linear chain)
379
+ _childToken[parentTokenId] = newTokenId;
380
+
381
+ // Emit full reason for off-chain indexing
382
+ emit TokenRevised(newTokenId, parentTokenId, originId, reason);
383
+ }
384
+
385
+ // ============ Query Functions ============
386
+
387
+ /**
388
+ * @notice Returns the token URI
389
+ * @param tokenId Token to query
390
+ */
391
+ function tokenURI(
392
+ uint256 tokenId
393
+ ) public view virtual override returns (string memory) {
394
+ if (!_exists(tokenId)) {
395
+ revert Errors.InvalidTokenId();
396
+ }
397
+ return _tokenURIs[tokenId];
398
+ }
399
+
400
+ /**
401
+ * @notice Returns the raw encoded content for a token
402
+ * @param tokenId Token to query
403
+ */
404
+ function tokenContent(
405
+ uint256 tokenId
406
+ ) external view returns (bytes memory) {
407
+ if (!_exists(tokenId)) {
408
+ revert Errors.InvalidTokenId();
409
+ }
410
+ return _tokenContents[tokenId];
411
+ }
412
+
413
+ /**
414
+ * @notice Returns the revision record for a token
415
+ * @param tokenId Token to query
416
+ */
417
+ function getRevisionRecord(
418
+ uint256 tokenId
419
+ ) external view returns (Types.RevisionRecord memory) {
420
+ if (!_exists(tokenId)) {
421
+ revert Errors.InvalidTokenId();
422
+ }
423
+ return _revisions[tokenId];
424
+ }
425
+
426
+ /**
427
+ * @notice Returns the full revision chain from origin to the given token
428
+ * @param tokenId Token to query (any token in the chain)
429
+ * @return chain Array of token IDs from origin to this token
430
+ */
431
+ function getRevisionChain(
432
+ uint256 tokenId
433
+ ) external view returns (uint256[] memory chain) {
434
+ if (!_exists(tokenId)) {
435
+ revert Errors.InvalidTokenId();
436
+ }
437
+
438
+ // First, count the chain length
439
+ uint256 length = 1;
440
+ uint256 current = tokenId;
441
+ while (_revisions[current].previousTokenId != 0) {
442
+ current = _revisions[current].previousTokenId;
443
+ length++;
444
+ }
445
+
446
+ // Build the chain array (origin first)
447
+ chain = new uint256[](length);
448
+ current = tokenId;
449
+ for (uint256 i = length; i > 0; i--) {
450
+ chain[i - 1] = current;
451
+ current = _revisions[current].previousTokenId;
452
+ }
453
+ }
454
+
455
+ /**
456
+ * @notice Returns the latest version in a revision chain
457
+ * @param originTokenId The origin token of the chain
458
+ * @return latestTokenId The leaf token (latest version)
459
+ */
460
+ function getLatestVersion(
461
+ uint256 originTokenId
462
+ ) external view returns (uint256 latestTokenId) {
463
+ if (!_exists(originTokenId)) {
464
+ revert Errors.InvalidTokenId();
465
+ }
466
+
467
+ latestTokenId = originTokenId;
468
+ while (_childToken[latestTokenId] != 0) {
469
+ latestTokenId = _childToken[latestTokenId];
470
+ }
471
+ }
472
+
473
+ /**
474
+ * @notice Returns the child token of a given token (0 if none)
475
+ * @param tokenId Token to query
476
+ * @return childTokenId The child token ID (0 if latest version)
477
+ */
478
+ function getChildToken(
479
+ uint256 tokenId
480
+ ) external view returns (uint256 childTokenId) {
481
+ if (!_exists(tokenId)) {
482
+ revert Errors.InvalidTokenId();
483
+ }
484
+ return _childToken[tokenId];
485
+ }
486
+
487
+ /**
488
+ * @notice Returns the total count of unique digital public assets
489
+ * @dev Only counts origin tokens (new DPAs), not revisions
490
+ * @return total The total number of unique DPAs
491
+ */
492
+ function getTotalAssets() external view returns (uint256 total) {
493
+ return _totalAssets;
494
+ }
495
+
496
+ /**
497
+ * @notice Checks if a token is the latest version in its chain
498
+ * @param tokenId Token to check
499
+ * @return isLatest True if the token has no child
500
+ */
501
+ function isLatestVersion(
502
+ uint256 tokenId
503
+ ) public view returns (bool isLatest) {
504
+ if (!_exists(tokenId)) {
505
+ revert Errors.InvalidTokenId();
506
+ }
507
+ return _childToken[tokenId] == 0;
508
+ }
509
+
510
+ /**
511
+ * @notice Returns the origin token ID for any token in a revision chain
512
+ * @param tokenId Token to query (can be any token in the chain)
513
+ * @return originTokenId The root/origin token of the chain
514
+ */
515
+ function getOriginToken(
516
+ uint256 tokenId
517
+ ) external view returns (uint256 originTokenId) {
518
+ if (!_exists(tokenId)) {
519
+ revert Errors.InvalidTokenId();
520
+ }
521
+ originTokenId = _revisions[tokenId].originTokenId;
522
+ if (originTokenId == 0) {
523
+ originTokenId = tokenId;
524
+ }
525
+ }
526
+
527
+ /**
528
+ * @notice Returns the version number of a token
529
+ * @param tokenId Token to query
530
+ * @return version The version number (1 for origin, increments on each revision)
531
+ */
532
+ function getVersion(
533
+ uint256 tokenId
534
+ ) external view returns (uint256 version) {
535
+ if (!_exists(tokenId)) {
536
+ revert Errors.InvalidTokenId();
537
+ }
538
+ return _revisions[tokenId].version;
539
+ }
540
+
541
+ // ============ Linked DPA Functions ============
542
+
543
+ /**
544
+ * @notice Links an external DPA contract to a token with a unique name
545
+ * @param tokenId Token to add the link to
546
+ * @param name Unique name for this link (per token, cannot be empty)
547
+ * @param dpaContract Address of the DPA contract to link (must be a contract, not self)
548
+ */
549
+ function linkDPA(
550
+ uint256 tokenId,
551
+ string calldata name,
552
+ address dpaContract
553
+ ) external onlyOrchestrator whenNotPaused nonReentrant {
554
+ if (!_exists(tokenId)) {
555
+ revert Errors.InvalidTokenId();
556
+ }
557
+ if (dpaContract == address(0)) {
558
+ revert Errors.ZeroAddress();
559
+ }
560
+
561
+ // Security: Validate name is not empty
562
+ if (bytes(name).length == 0) {
563
+ revert Errors.EmptyLinkName();
564
+ }
565
+
566
+ // Security: Cannot link to self
567
+ if (dpaContract == address(this)) {
568
+ revert Errors.SelfLink();
569
+ }
570
+
571
+ // Security: Must be a contract, not an EOA
572
+ if (dpaContract.code.length == 0) {
573
+ revert Errors.NotAContract();
574
+ }
575
+
576
+ // Security: Must implement IDPA interface (ERC-165 check)
577
+ try
578
+ IERC165(dpaContract).supportsInterface(type(IDPA).interfaceId)
579
+ returns (bool supported) {
580
+ if (!supported) {
581
+ revert Errors.NotADPAContract();
582
+ }
583
+ } catch {
584
+ revert Errors.NotADPAContract();
585
+ }
586
+
587
+ bytes32 nameHash = keccak256(bytes(name));
588
+
589
+ // Check if name already exists for this token
590
+ if (_linkedDPAs[tokenId][nameHash] != address(0)) {
591
+ revert Errors.DuplicateLinkName();
592
+ }
593
+
594
+ // Store the link
595
+ _linkedDPAs[tokenId][nameHash] = dpaContract;
596
+ _linkedDPANames[tokenId].push(nameHash);
597
+
598
+ emit DPALinked(tokenId, nameHash, dpaContract, name);
599
+ }
600
+
601
+ /**
602
+ * @notice Removes a linked DPA contract by name
603
+ * @param tokenId Token to remove the link from
604
+ * @param name Name of the link to remove
605
+ */
606
+ function unlinkDPA(
607
+ uint256 tokenId,
608
+ string calldata name
609
+ ) external onlyOrchestrator whenNotPaused nonReentrant {
610
+ if (!_exists(tokenId)) {
611
+ revert Errors.InvalidTokenId();
612
+ }
613
+
614
+ bytes32 nameHash = keccak256(bytes(name));
615
+ address dpaContract = _linkedDPAs[tokenId][nameHash];
616
+
617
+ if (dpaContract == address(0)) {
618
+ revert Errors.LinkNotFound();
619
+ }
620
+
621
+ // Remove from mapping
622
+ delete _linkedDPAs[tokenId][nameHash];
623
+
624
+ // Remove from names array (swap and pop for gas efficiency)
625
+ bytes32[] storage names = _linkedDPANames[tokenId];
626
+ for (uint256 i = 0; i < names.length; i++) {
627
+ if (names[i] == nameHash) {
628
+ names[i] = names[names.length - 1];
629
+ names.pop();
630
+ break;
631
+ }
632
+ }
633
+
634
+ emit DPAUnlinked(tokenId, nameHash, dpaContract);
635
+ }
636
+
637
+ /**
638
+ * @notice Returns the linked DPA contract address by name
639
+ * @param tokenId Token to query
640
+ * @param name Name of the link
641
+ * @return dpaContract The linked DPA contract address (zero if not found)
642
+ */
643
+ function getLinkedDPA(
644
+ uint256 tokenId,
645
+ string calldata name
646
+ ) external view returns (address dpaContract) {
647
+ if (!_exists(tokenId)) {
648
+ revert Errors.InvalidTokenId();
649
+ }
650
+ bytes32 nameHash = keccak256(bytes(name));
651
+ return _linkedDPAs[tokenId][nameHash];
652
+ }
653
+
654
+ /**
655
+ * @notice Returns all link name hashes for a token
656
+ * @param tokenId Token to query
657
+ * @return nameHashes Array of name hashes
658
+ */
659
+ function getLinkedDPANameHashes(
660
+ uint256 tokenId
661
+ ) external view returns (bytes32[] memory nameHashes) {
662
+ if (!_exists(tokenId)) {
663
+ revert Errors.InvalidTokenId();
664
+ }
665
+ return _linkedDPANames[tokenId];
666
+ }
667
+
668
+ /**
669
+ * @notice Returns the number of linked DPAs for a token
670
+ * @param tokenId Token to query
671
+ * @return count The number of linked DPAs
672
+ */
673
+ function getLinkedDPACount(
674
+ uint256 tokenId
675
+ ) external view returns (uint256 count) {
676
+ if (!_exists(tokenId)) {
677
+ revert Errors.InvalidTokenId();
678
+ }
679
+ return _linkedDPANames[tokenId].length;
680
+ }
681
+
682
+ /**
683
+ * @notice Checks if a token has any linked DPAs
684
+ * @param tokenId Token to check
685
+ * @return hasLinks True if the token has linked DPAs
686
+ */
687
+ function hasLinkedDPAs(
688
+ uint256 tokenId
689
+ ) external view returns (bool hasLinks) {
690
+ if (!_exists(tokenId)) {
691
+ revert Errors.InvalidTokenId();
692
+ }
693
+ return _linkedDPANames[tokenId].length > 0;
694
+ }
695
+
696
+ // ============ Internal Functions ============
697
+
698
+ /**
699
+ * @dev Validates encoded content - override in derived contracts
700
+ * @param content The encoded content to validate
701
+ */
702
+ function _validateContent(bytes calldata content) internal view virtual {
703
+ // Default: no validation. Override for specific validation logic.
704
+ }
705
+
706
+ /**
707
+ * @dev Override to start token IDs at 1 instead of 0
708
+ */
709
+ function _startTokenId() internal view virtual override returns (uint256) {
710
+ return 1;
711
+ }
712
+
713
+ /**
714
+ * @dev Override to prevent burning - DPA tokens are immutable public records
715
+ */
716
+ function _beforeTokenTransfers(
717
+ address from,
718
+ address to,
719
+ uint256 startTokenId,
720
+ uint256 quantity
721
+ ) internal virtual override {
722
+ super._beforeTokenTransfers(from, to, startTokenId, quantity);
723
+
724
+ // Prevent burning (to == address(0) and from != address(0))
725
+ if (to == address(0) && from != address(0)) {
726
+ revert Errors.BurnDisabled();
727
+ }
728
+ }
729
+
730
+ /**
731
+ * @dev Override to support ERC-165 interface detection for IDPA
732
+ */
733
+ function supportsInterface(
734
+ bytes4 interfaceId
735
+ ) public view virtual override returns (bool) {
736
+ return
737
+ interfaceId == type(IDPA).interfaceId ||
738
+ super.supportsInterface(interfaceId);
739
+ }
740
+ }