Contract Architecture
This document provides a comprehensive explanation of how the Creator Core contracts work, including the proxy pattern, extension system, and internal mechanisms.
Table of Contents
- Proxy Pattern Architecture
- Contract Inheritance Hierarchy
- Extension System
- Minting Mechanisms
- Royalty System
- Transfer Control
- Token URI Resolution
- Access Control
- Data Flow Examples
Proxy Pattern Architecture
Why Use a Proxy?
The ERC721CreatorImplementation contract is large and exceeds the 24KB EVM contract size limit when compiled. The proxy pattern solves this by:
- Separating Storage from Logic: The proxy contract (small) stores state, while the implementation (large) contains logic
- Enabling Upgrades: The implementation can be upgraded without changing the proxy address
- Gas Efficiency: Multiple proxies can share the same implementation
Three-Component System
When deploying an upgradeable collection, three contracts are created:
┌─────────────────────────────────────────────────────────┐
│ Proxy (0x6302C5F1F2E3d0e4D5ae5aeB88bd8044c88Eef9A) │
│ - Small contract (< 24KB) │
│ - Stores state (owner, token balances, etc.) │
│ - Delegates all calls to implementation │
│ - Users interact with this address │
└──────────────────┬──────────────────────────────────────┘
│ delegatecall
▼
┌─────────────────────────────────────────────────────────┐
│ Implementation (0x0C1f9d0b4b92411B145E70A33052AE87D19e99c4) │
│ - Contains all business logic │
│ - No state storage (uses proxy's storage) │
│ - Can be upgraded │
└─────────────────────────────────────────────────────────┘
│
│ controlled by
▼
┌─────────────────────────────────────────────────────────┐
│ ProxyAdmin (0xDF6c66d24C6DDBC9CcfDc74A243E8e098981a26E) │
│ - Owns the proxy │
│ - Only owner can upgrade implementation │
│ - Protects against unauthorized upgrades │
└─────────────────────────────────────────────────────────┘
How Delegation Works
When a user calls a function on the proxy:
- User calls:
proxy.mintBase(to) - Proxy receives call: Checks if it's an admin function (upgrade)
- If not admin: Uses
delegatecallto forward to implementation - Implementation executes: Logic runs in proxy's storage context
- State changes: Written to proxy's storage, not implementation's
- Return value: Passed back through proxy to user
Storage Layout
The proxy uses a specific storage layout to avoid collisions:
// Proxy storage slot (OpenZeppelin standard)
bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
bytes32 private constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
// Implementation storage (ERC721CreatorImplementation)
mapping(uint256 => address) private _owners; // Slot 0
mapping(address => uint256) private _balances; // Slot 1
mapping(uint256 => address) private _tokenApprovals; // Slot 2
// ... etc
The implementation's storage slots start after the proxy's reserved slots, preventing conflicts.
Contract Inheritance Hierarchy
The ERC721CreatorImplementation contract inherits from multiple base contracts:
ERC721CreatorImplementation
├── AdminControlUpgradeable
│ └── OwnableUpgradeable (OpenZeppelin)
│ └── Initializable
├── ERC721Upgradeable
│ ├── ERC721Core
│ │ ├── ERC721 (OpenZeppelin)
│ │ └── IERC721Metadata
│ └── ReentrancyGuardUpgradeable
└── ERC721CreatorCore
└── CreatorCore
└── ICreatorCore
Component Responsibilities
AdminControlUpgradeable
- Manages owner and admin addresses
- Provides
adminRequiredmodifier - Handles ownership transfers
ERC721Upgradeable
- Standard ERC721 functionality (mint, transfer, approve)
- Token ownership tracking
- Metadata support (name, symbol, tokenURI)
- Reentrancy protection
ERC721CreatorCore
- Extension management
- Extension-based minting
- Transfer approval hooks
- Token extension tracking
CreatorCore
- Base extension registry
- URI management (base, prefix, per-token)
- Royalty configuration
- Mint permissions
Extension System
The extension system allows external contracts to extend the functionality of the creator contract.
Extension Registration
// Admin registers an extension
creatorCore.registerExtension(extensionAddress, baseURI, false);
What happens internally:
- Validation: Checks extension is a contract and not blacklisted
- Index Assignment: Assigns unique index (1-65535) to extension
- Storage Updates:
_extensions.add(extension); // Add to set _extensionToIndex[extension] = index; // Map extension → index _indexToExtension[index] = extension; // Map index → extension _extensionBaseURI[extension] = baseURI; // Store base URI - Transfer Approval: Sets up transfer approval delegation if extension supports it
Extension Minting
When an extension mints a token:
// Extension calls
uint256 tokenId = creatorCore.mintExtension(to);
Internal flow:
- Require Extension:
requireExtension()checksmsg.senderis registered - Check Permissions: Calls mint permission contract if configured
- Pre-Mint Hook: Calls
_preMintExtension(to, tokenId) - Mint Token: Calls
_safeMint(to, tokenId, data)dataencodes:(extensionIndex << 16) | extensionData
- Store Extension: Token's data field stores extension index
- Post-Mint: Extension can set custom URI or royalties
Extension Token Data
Each token stores its originating extension in the _tokenData mapping:
struct TokenData {
address owner; // Token owner
uint96 data; // Extension index (lower 16 bits) + custom data (upper 80 bits)
}
mapping(uint256 => TokenData) private _tokenData;
Decoding token data:
- Lower 16 bits: Extension index (0 = base mint, >0 = extension index)
- Upper 80 bits: Custom data (set by extension)
Extension Capabilities
Extensions can implement interfaces to gain additional capabilities:
ICreatorExtensionTokenURI
Override token URI generation:
function tokenURI(address creator, uint256 tokenId)
external view returns (string memory);
IERC721CreatorExtensionApproveTransfer
Control transfers before execution:
function approveTransfer(address operator, address from, address to, uint256 tokenId)
external returns (bool);
IERC721CreatorExtensionBurnable
Handle burn events:
function onBurn(address owner, uint256 tokenId) external;
ICreatorExtensionRoyalties
Define royalties for extension-minted tokens:
function getRoyalties(address creator, uint256 tokenId)
external view returns (address payable[] memory, uint256[] memory);
Minting Mechanisms
Base Minting (Admin-Only)
Base minting is for direct admin-controlled minting:
// Single mint
uint256 tokenId = creatorCore.mintBase(to, "ipfs://...");
// Batch mint
uint256[] memory tokenIds = creatorCore.mintBaseBatch(to, 100);
Characteristics:
- Requires
adminRequiredmodifier - Token extension index = 0 (no extension)
- No mint permission checks
- Direct URI assignment possible
Gas costs:
- Single mint: ~65,000 gas
- Batch mint (100 tokens): ~65,000 gas (same transaction, amortized)
Extension Minting
Extension minting allows registered extensions to mint:
// Extension calls
uint256 tokenId = creatorCore.mintExtension(to);
Characteristics:
- Requires extension to be registered
- Token extension index > 0 (stored in token data)
- Mint permission checks (if configured)
- Extension can set custom URI after mint
Flow:
Extension Contract
│
├─> creatorCore.mintExtension(to)
│ │
│ ├─> requireExtension() [check msg.sender is registered]
│ ├─> _checkMintPermissions(to, tokenId) [if permissions set]
│ ├─> _preMintExtension(to, tokenId) [hook]
│ ├─> _safeMint(to, tokenId, extensionIndex) [mint with extension data]
│ └─> return tokenId
│
└─> extension.setTokenURI(tokenId, customURI) [optional]
Token ID Assignment
Token IDs are assigned sequentially:
uint256 private _tokenCount; // Current token count
// When minting
++_tokenCount;
tokenId = _tokenCount; // First token is ID 1
Important: Token IDs start at 1, not 0. This is standard ERC721 practice.
Royalty System
The contract supports multiple royalty standards and configuration levels.
Royalty Standards
- EIP-2981 (
royaltyInfo): Standard royalty interface - Manifold Registry: Via
getRoyalties() - Rarible V2: Via
getFeeRecipients()andgetFeeBps()
Royalty Configuration Levels
Royalties can be set at three levels (checked in order):
-
Per-Token Royalties (highest priority)
creatorCore.setRoyalties(tokenId, receivers, basisPoints); -
Extension Royalties (for extension-minted tokens)
creatorCore.setRoyaltiesExtension(extension, receivers, basisPoints); -
Default Royalties (fallback for all tokens)
creatorCore.setRoyalties(receivers, basisPoints);
Royalty Resolution
When royaltyInfo(tokenId, salePrice) is called:
function royaltyInfo(uint256 tokenId, uint256 value)
external view returns (address, uint256)
{
// 1. Check per-token royalties
if (_tokenRoyalty[tokenId].receivers.length > 0) {
return _getRoyaltyInfo(_tokenRoyalty[tokenId], value);
}
// 2. Check extension royalties (if token has extension)
address extension = _tokenExtension(tokenId);
if (extension != address(0) && _extensionRoyalty[extension].receivers.length > 0) {
return _getRoyaltyInfo(_extensionRoyalty[extension], value);
}
// 3. Fall back to default royalties
return _getRoyaltyInfo(_defaultRoyalty, value);
}
Basis Points
Royalties are specified in basis points (bps):
- 100 bps = 1%
- 500 bps = 5%
- 1000 bps = 10%
- 10000 bps = 100% (maximum)
Example:
address payable[] memory receivers = new address payable[](1);
receivers[0] = payable(0x1234...);
uint256[] memory basisPoints = new uint256[](1);
basisPoints[0] = 500; // 5% royalty
creatorCore.setRoyalties(receivers, basisPoints);
Transfer Control
The contract provides hooks for extensions to control transfers.
Transfer Approval Flow
When a transfer is initiated:
User calls transferFrom(from, to, tokenId)
│
├─> _beforeTokenTransfer(from, to, tokenId, data)
│ │
│ ├─> Extract extension index from token data
│ ├─> Get extension address from index
│ └─> _approveTransfer(from, to, tokenId, extension)
│ │
│ ├─> If extension has approve transfer enabled:
│ │ └─> extension.approveTransfer(operator, from, to, tokenId)
│ │ └─> Must return true or revert
│ │
│ └─> If base approve transfer set:
│ └─> _approveTransferBase.approveTransfer(...)
│
└─> Execute transfer (if approved)
Extension Transfer Approval
Extensions can implement IERC721CreatorExtensionApproveTransfer:
interface IERC721CreatorExtensionApproveTransfer {
function approveTransfer(
address operator, // Who initiated the transfer
address from, // Current owner
address to, // New owner
uint256 tokenId // Token being transferred
) external returns (bool);
}
Use cases:
- Restrict transfers during certain periods
- Require additional approvals
- Implement cooldown periods
- Enforce whitelist requirements
Base Transfer Approval
A single contract can be set as the base transfer approver:
creatorCore.setApproveTransfer(approvalContract);
This contract will be called for all transfers, regardless of extension.
Token URI Resolution
The contract supports flexible URI management with multiple resolution strategies.
URI Resolution Order
When tokenURI(tokenId) is called:
-
Extension URI (if token has extension and extension implements
ICreatorExtensionTokenURI)extension.tokenURI(creatorAddress, tokenId) -
Per-Token URI (if explicitly set)
_tokenURIs[tokenId] -
Base URI + Token ID (if base URI is set)
string(abi.encodePacked(_baseTokenURI, Strings.toString(tokenId))) -
Extension Base URI + Token ID (if token has extension)
string(abi.encodePacked(_extensionBaseURI[extension], Strings.toString(tokenId)))
URI Patterns
Base URI Pattern
creatorCore.setBaseTokenURI("https://api.example.com/metadata/");
// Token 1: https://api.example.com/metadata/1
// Token 2: https://api.example.com/metadata/2
Prefix Pattern
creatorCore.setTokenURIPrefix("ipfs://");
creatorCore.setTokenURI(tokenId, "QmHash...");
// Result: ipfs://QmHash...
Per-Token URI
creatorCore.setTokenURI(tokenId, "https://example.com/token/1.json");
Extension URI Override
// Extension implements ICreatorExtensionTokenURI
function tokenURI(address creator, uint256 tokenId)
external view returns (string memory)
{
// Dynamic URI generation
return string(abi.encodePacked(
"https://api.example.com/token/",
Strings.toString(tokenId),
"?v=",
Strings.toString(block.timestamp)
));
}
Access Control
The contract uses a two-tier access control system.
Owner
- Single address with full control
- Can transfer ownership
- Can add/remove admins
- Can perform all admin functions
Admins
- Multiple addresses with admin privileges
- Can call functions with
adminRequiredmodifier - Cannot transfer ownership
- Cannot add/remove other admins (unless they're also owner)
Admin Functions
Functions protected by adminRequired:
registerExtension()/unregisterExtension()blacklistExtension()mintBase()/mintBaseBatch()setRoyalties()setBaseTokenURI()/setTokenURI()setMintPermissions()
Access Control Flow
modifier adminRequired() {
require(
owner() == msg.sender ||
admins.contains(msg.sender),
"Admin access required"
);
_;
}
Data Flow Examples
Example 1: Base Minting
User (Admin)
│
├─> creatorCore.mintBase(to, "ipfs://QmHash")
│ │
│ ├─> adminRequired() [check]
│ ├─> _mintBase(to, "ipfs://QmHash", 0)
│ │ │
│ │ ├─> _preMintBase(to, tokenId) [hook]
│ │ ├─> ++_tokenCount
│ │ ├─> _safeMint(to, tokenId, 0) [extension index = 0]
│ │ │ │
│ │ │ ├─> _owners[tokenId] = to
│ │ │ ├─> _balances[to]++
│ │ │ └─> emit Transfer(address(0), to, tokenId)
│ │ │
│ │ └─> _tokenURIs[tokenId] = "ipfs://QmHash"
│ │
│ └─> return tokenId
│
└─> Token minted with ID = _tokenCount, extension = 0
Example 2: Extension Minting with Transfer Control
Extension Contract
│
├─> creatorCore.mintExtension(to)
│ │
│ ├─> requireExtension() [check registered]
│ ├─> _checkMintPermissions(to, tokenId) [if set]
│ ├─> _mintExtension(to, "", 0, 0)
│ │ │
│ │ ├─> ++_tokenCount
│ │ ├─> extensionIndex = _extensionToIndex[msg.sender]
│ │ ├─> _safeMint(to, tokenId, extensionIndex << 16)
│ │ │ │
│ │ │ └─> Token data stores extension index
│ │ │
│ │ └─> return tokenId
│ │
│ └─> return tokenId
│
├─> User tries to transfer token
│ │
│ ├─> creatorCore.transferFrom(from, to, tokenId)
│ │ │
│ │ ├─> _beforeTokenTransfer(from, to, tokenId, data)
│ │ │ │
│ │ │ ├─> Extract extension index from token data
│ │ │ ├─> Get extension address
│ │ │ └─> extension.approveTransfer(operator, from, to, tokenId)
│ │ │ │
│ │ │ └─> Extension logic (e.g., check cooldown)
│ │ │ └─> return true/false
│ │ │
│ │ └─> Execute transfer (if approved)
│ │
│ └─> Transfer succeeds or reverts based on extension approval
Example 3: Royalty Resolution
Marketplace calls royaltyInfo(tokenId, salePrice)
│
├─> creatorCore.royaltyInfo(tokenId, salePrice)
│ │
│ ├─> Check _tokenRoyalty[tokenId]
│ │ └─> If set: return token-specific royalty
│ │
│ ├─> Check extension (if token has one)
│ │ ├─> Get extension from token data
│ │ └─> Check _extensionRoyalty[extension]
│ │ └─> If set: return extension royalty
│ │
│ └─> Return _defaultRoyalty (fallback)
│
└─> Marketplace pays royalty to returned address
Example 4: Token URI Resolution
User calls tokenURI(tokenId)
│
├─> creatorCore.tokenURI(tokenId)
│ │
│ ├─> Get extension from token data
│ │ └─> If extension exists:
│ │ └─> Check if extension implements ICreatorExtensionTokenURI
│ │ └─> If yes: return extension.tokenURI(creator, tokenId)
│ │
│ ├─> Check _tokenURIs[tokenId]
│ │ └─> If set: return per-token URI
│ │
│ ├─> Check _baseTokenURI
│ │ └─> If set: return baseURI + tokenId
│ │
│ └─> Check extension base URI
│ └─> If set: return extensionBaseURI + tokenId
│
└─> Return resolved URI
Storage Layout
Proxy Storage (OpenZeppelin)
// Slot 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
address private _implementation;
// Slot 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
address private _admin;
Implementation Storage
// ERC721 Storage
mapping(uint256 => address) private _owners; // Slot 0
mapping(address => uint256) private _balances; // Slot 1
mapping(uint256 => address) private _tokenApprovals; // Slot 2
mapping(address => mapping(address => bool)) private _operatorApprovals; // Slot 3
// ERC721Metadata Storage
string private _name; // Slot 4
string private _symbol; // Slot 5
mapping(uint256 => string) internal _tokenURIs; // Slot 6
// CreatorCore Storage
EnumerableSet.AddressSet private _extensions; // Slot 7
EnumerableSet.AddressSet private _blacklistedExtensions; // Slot 8
mapping(address => string) internal _extensionBaseURI; // Slot 9
mapping(uint256 => string) internal _tokenURIs; // Slot 10 (overlaps with ERC721)
mapping(address => RoyaltyConfig) internal _extensionRoyalty; // Slot 11
mapping(uint256 => RoyaltyConfig) internal _tokenRoyalty; // Slot 12
RoyaltyConfig internal _defaultRoyalty; // Slot 13
// ERC721CreatorCore Storage
uint16 private _extensionCounter; // Slot 14
mapping(address => uint16) internal _extensionToIndex; // Slot 15
mapping(uint16 => address) internal _indexToExtension; // Slot 16
mapping(address => bool) internal _extensionApproveTransfers; // Slot 17
mapping(address => address) internal _extensionPermissions; // Slot 18
// AdminControl Storage
address private _owner; // Slot 19
EnumerableSet.AddressSet private _admins; // Slot 20
Security Considerations
Proxy Upgrade Safety
- Only ProxyAdmin owner can upgrade
- Implementation contract should be verified before upgrade
- Storage layout must remain compatible
- Use
storagekeyword in implementation to reference proxy storage
Extension Security
- Extensions are external contracts - validate before registration
- Blacklist mechanism prevents malicious extensions
- Transfer approval can block malicious transfers
- Mint permissions can restrict who can mint
Reentrancy Protection
nonReentrantmodifier on minting functions- Transfer approval checks before state changes
- Safe external calls with proper error handling
Gas Optimization
Batch Operations
Always use batch functions when minting multiple tokens:
mintBaseBatch(): Single transaction for multiple mintssetTokenURI()with arrays: Batch URI updates
Storage Optimization
- Use base URI instead of per-token URIs when possible
- Extension indices (uint16) instead of addresses save gas
- EnumerableSet for efficient extension management
Proxy Pattern Benefits
- Implementation deployed once, reused by multiple proxies
- Proxy deployment: ~500k gas
- Implementation deployment: ~5M gas (one-time)
Related Documentation
- Deployment Guide - How to deploy contracts
- Integration Guide - How to integrate with marketplaces
- Deployments - Tracked deployments
- README - Overview and quick start