@dpa-oss/dpa
v1.0.3
Published
DPA (Digital Public Asset) Smart Contracts
Readme
DPA Contracts
Digital Public Asset (DPA) – An abstract ERC721A-based smart contract framework for creating immutable, revisable, and composable digital public assets on the blockchain.
Overview
DPA (Digital Public Asset) is an abstract smart contract that extends ERC721A to provide a secure and gas-efficient foundation for creating tokenized public records with built-in revision tracking and cross-contract composition.
Key Features
- 🔒 Orchestrator-Controlled Minting – Only authorized orchestrators can mint/revise tokens
- 📝 Protocol-Enforced Revision Tracking – Immutable linked-list structure for full traceability
- 🔗 Cross-Contract Composition – Link DPA contracts with named, unique references
- ⚡ Gas-Efficient Batch Minting – ERC721A-powered batch operations
- 🛡️ Security Hardened – ReentrancyGuard, Pausable, burn prevention, ERC-165 interface detection
- 📦 Generic Content Storage – Store arbitrary bytes, decoded by implementers
Installation
npm installDependencies
- @openzeppelin/contracts
^5.0.0 - erc721a
^4.3.0 - hardhat
^2.22.0
Quick Start
Compile Contracts
npm run compileRun Tests
npm run testClean Build Artifacts
npm run cleanUsage
Extending DPA
DPA is an abstract contract – you must create a concrete implementation:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./DPA.sol";
contract MyAssetDPA is DPA {
struct MyContent {
uint256 value;
string description;
}
constructor(
address orchestrator_
) DPA("My Asset DPA", "MY-DPA", orchestrator_) {}
function _validateContent(bytes calldata content) internal pure override {
// Validate that content can be decoded
abi.decode(content, (MyContent));
}
function getMyContent(uint256 tokenId) external view returns (MyContent memory) {
bytes memory raw = this.tokenContent(tokenId);
return abi.decode(raw, (MyContent));
}
}Minting Tokens
// Single mint
uint256 tokenId = dpa.mint(
recipientAddress,
"ipfs://metadata-uri",
encodedContent
);
// Batch mint
uint256 startTokenId = dpa.batchMint(
recipientAddress,
["ipfs://uri1", "ipfs://uri2"],
[encodedContent1, encodedContent2]
);Creating Revisions
uint256 newTokenId = dpa.revise(
parentTokenId,
"ipfs://new-metadata-uri",
newEncodedContent,
"Reason for revision"
);Linking DPA Contracts
// Link another DPA contract
dpa.linkDPA(tokenId, "expenditures", expenditureContractAddress);
// Query linked contract
address linked = dpa.getLinkedDPA(tokenId, "expenditures");
// Unlink
dpa.unlinkDPA(tokenId, "expenditures");Architecture
Contract Structure
contracts/
├── DPA.sol # Abstract base contract
├── shared/
│ ├── IDPA.sol # Interface definition (extends IERC165)
│ ├── Types.sol # Shared data structures
│ └── Errors.sol # Custom error definitions
└── examples/
└── AssetDPA.sol # Example implementationInheritance Diagram
ERC721A
│
├── Ownable (OpenZeppelin)
├── Pausable (OpenZeppelin)
├── ReentrancyGuard (OpenZeppelin)
│
└── DPA (Abstract)
│
└── Your Implementation (e.g., AssetDPA)Interface
IDPA Interface
The IDPA interface extends IERC165 and defines the standard functions:
| Function | Description |
|----------|-------------|
| orchestrator() | Returns the current orchestrator address |
| setOrchestrator(address) | Updates the orchestrator (owner only) |
| pause() / unpause() | Pause/unpause operations (owner only) |
| mint(to, uri, content) | Mints a single token |
| mintWithOwner(to, owner_, uri, content) | Mints with explicit owner |
| batchMint(to, uris, contents) | Batch mints tokens |
| revise(parentTokenId, uri, content, reason) | Creates a revision |
| tokenURI(tokenId) | Returns token metadata URI |
| tokenContent(tokenId) | Returns raw encoded content |
| getRevisionRecord(tokenId) | Returns revision metadata |
| getRevisionChain(tokenId) | Returns full revision history |
| getLatestVersion(originTokenId) | Returns latest token in chain |
| getChildToken(tokenId) | Returns child revision |
| getTotalAssets() | Returns unique asset count |
| isLatestVersion(tokenId) | Checks if token is latest |
| getOriginToken(tokenId) | Returns origin token ID |
| getVersion(tokenId) | Returns version number |
RevisionRecord Structure
struct RevisionRecord {
uint256 previousTokenId; // Parent revision (0 if origin)
uint256 originTokenId; // Root of the revision chain
uint256 version; // Version number (1 for origin)
bytes32 reasonHash; // Keccak256 hash of revision reason
uint256 timestamp; // Block timestamp when created
address actor; // Address that created this revision
}Security
Built-in Protections
| Feature | Description | |---------|-------------| | ReentrancyGuard | Prevents reentrancy attacks on mint/revise/link operations | | Pausable | Owner can pause all minting and revision operations | | Burn Prevention | Tokens cannot be burned – ensures permanent public records | | ERC-165 Interface Detection | Validates linked contracts implement IDPA | | Zero Address Checks | Prevents setting orchestrator or linking to zero address | | Self-Link Prevention | Contracts cannot link to themselves | | Contract Address Validation | Only contracts (not EOAs) can be linked | | Latest Version Enforcement | Only latest version tokens can be revised |
Custom Errors
error NotOrchestrator(); // Caller is not the orchestrator
error InvalidTokenId(); // Token does not exist
error InvalidRevision(); // Revision chain violation
error BatchMintFailed(); // Batch minting error
error ArrayLengthMismatch(); // Arrays length mismatch
error ZeroAddress(); // Zero address provided
error NotLatestVersion(); // Token is not the latest version
error DuplicateLinkName(); // Link name already exists
error LinkNotFound(); // Link name not found
error EmptyLinkName(); // Link name cannot be empty
error NotAContract(); // Address is not a contract
error SelfLink(); // Cannot link to self
error BurnDisabled(); // Token burning is disabled
error NotADPAContract(); // Contract does not implement IDPAEvents
event TokenMinted(uint256 indexed tokenId, address indexed to, string uri);
event BatchMinted(uint256 indexed startTokenId, uint256 quantity, address indexed to);
event TokenRevised(uint256 indexed newTokenId, uint256 indexed parentTokenId, uint256 indexed originTokenId, string reason);
event OrchestratorUpdated(address indexed previousOrchestrator, address indexed newOrchestrator);
event DPALinked(uint256 indexed tokenId, bytes32 indexed nameHash, address indexed dpaContract, string name);
event DPAUnlinked(uint256 indexed tokenId, bytes32 indexed nameHash, address indexed dpaContract);Gas Optimization
DPA leverages ERC721A for significant gas savings on batch operations:
| Operation | Gas Savings | |-----------|-------------| | Batch Minting | Up to 90% vs ERC721 | | Optimized Storage | Packed revision records | | Hash-Based Reason Storage | Full reason emitted in events, hash stored on-chain |
Generate a gas report:
npm run testGas report is saved to gas-report.txt.
Development
Prerequisites
- Node.js >= 18.x
- npm >= 9.x
Project Structure
dpa-contracts/
├── contracts/ # Solidity source files
├── test/ # Test files
├── artifacts/ # Compiled contracts (generated)
├── typechain-types/ # TypeScript bindings (generated)
├── hardhat.config.ts # Hardhat configuration
├── package.json # Project dependencies
└── tsconfig.json # TypeScript configurationHardhat Configuration
// hardhat.config.ts
const config: HardhatUserConfig = {
solidity: {
version: "0.8.20",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
gasReporter: {
enabled: true,
currency: "USD",
outputFile: "gas-report.txt",
noColors: true,
},
};Example Implementation
See contracts/examples/AssetDPA.sol for a complete example:
contract AssetDPA is DPA {
enum AssetType { Enum1, Enum2 }
struct AssetContent {
uint256 amount;
AssetType assetType;
}
constructor(address orchestrator_)
DPA("Asset DPA", "ASSET-DPA", orchestrator_) {}
function _validateContent(bytes calldata content) internal pure override {
abi.decode(content, (AssetContent));
}
function getAssetContent(uint256 tokenId) external view returns (AssetContent memory) {
return abi.decode(this.tokenContent(tokenId), (AssetContent));
}
function encodeAssetContent(uint256 amount, AssetType assetType) external pure returns (bytes memory) {
return abi.encode(AssetContent({amount: amount, assetType: assetType}));
}
}License
This project is licensed under the MIT License.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Author
Jason Cruz
Related Projects
- ERC721A – Gas-efficient ERC721 implementation
- OpenZeppelin Contracts – Security-audited smart contract library
