LPsLux Proposals
LRC Standards
LP-3211

Media Content NFT Standard

Final

Standard for media and content NFTs with licensing, royalties, and metadata on Lux Network

Category
LRC
Created
2025-01-23

Abstract

This LP defines a standard for media content NFTs on the Lux Network, enabling creators to tokenize media (images, videos, audio, documents) with built-in licensing terms, royalty distribution, and rich metadata. Based on the Zora Media protocol, it provides a framework for content monetization, rights management, and collaborative creation.

Motivation

Media content NFT standards enable:

  1. Content Monetization: Direct creator-to-consumer sales
  2. Rights Management: On-chain licensing terms
  3. Royalty Distribution: Automatic splits for collaborators
  4. Content Integrity: Immutable content hashes
  5. Metadata Standards: Rich, queryable metadata

Specification

Core Media Interface

interface ILuxMedia {
    struct MediaData {
        string tokenURI;        // Metadata URI
        string metadataURI;     // Extended metadata
        bytes32 contentHash;    // Content hash for verification
        bytes32 metadataHash;   // Metadata hash
    }
    
    struct BidShares {
        uint256 creator;        // Creator share in basis points
        uint256 owner;          // Current owner share
        uint256 prevOwner;      // Previous owner share
    }
    
    struct Ask {
        uint256 amount;         // Ask price
        address currency;       // Currency address (0 for native)
    }
    
    struct Bid {
        uint256 amount;         // Bid amount
        address currency;       // Currency address
        address bidder;         // Bidder address
        address recipient;      // NFT recipient
        uint256 expiry;        // Bid expiration
    }
    
    // Events
    event MediaMinted(
        uint256 indexed tokenId,
        address indexed creator,
        string tokenURI,
        string metadataURI,
        bytes32 contentHash,
        bytes32 metadataHash
    );
    
    event MediaUpdated(
        uint256 indexed tokenId,
        address indexed owner,
        string tokenURI,
        string metadataURI,
        bytes32 contentHash,
        bytes32 metadataHash
    );
    
    event BidSharesUpdated(
        uint256 indexed tokenId,
        BidShares bidShares
    );
    
    event AskCreated(uint256 indexed tokenId, Ask ask);
    event AskRemoved(uint256 indexed tokenId, Ask ask);
    event BidCreated(uint256 indexed tokenId, Bid bid);
    event BidRemoved(uint256 indexed tokenId, Bid bid);
    event BidFinalized(uint256 indexed tokenId, Bid bid);
    
    // Core functions
    function mint(
        MediaData calldata data,
        BidShares calldata bidShares
    ) external returns (uint256);
    
    function mintWithSig(
        address creator,
        MediaData calldata data,
        BidShares calldata bidShares,
        bytes calldata sig
    ) external returns (uint256);
    
    function updateTokenURI(
        uint256 tokenId,
        string calldata tokenURI
    ) external;
    
    function updateTokenMetadataURI(
        uint256 tokenId,
        string calldata metadataURI
    ) external;
    
    function setBidShares(
        uint256 tokenId,
        BidShares calldata bidShares
    ) external;
    
    // Market functions
    function setAsk(uint256 tokenId, Ask calldata ask) external;
    function removeAsk(uint256 tokenId) external;
    function setBid(uint256 tokenId, Bid calldata bid) external;
    function removeBid(uint256 tokenId) external;
    function acceptBid(uint256 tokenId, Bid calldata expectedBid) external;
    
    // View functions
    function tokenMediaData(uint256 tokenId) external view returns (MediaData memory);
    function bidSharesForToken(uint256 tokenId) external view returns (BidShares memory);
    function currentAskForToken(uint256 tokenId) external view returns (Ask memory);
    function currentBidForToken(uint256 tokenId) external view returns (Bid memory);
}

Licensing Extension

interface IMediaLicensing is ILuxMedia {
    enum LicenseType {
        AllRightsReserved,
        NonCommercial,
        Commercial,
        CreativeCommons,
        Custom
    }
    
    struct License {
        LicenseType licenseType;
        string licenseURI;
        uint256 commercialUseFee;
        uint256 derivativeWorkFee;
        bool allowDerivatives;
        bool allowCommercialUse;
        uint256 expirationTime;
    }
    
    event LicenseSet(
        uint256 indexed tokenId,
        LicenseType licenseType,
        string licenseURI
    );
    
    event LicenseActivated(
        uint256 indexed tokenId,
        address indexed licensee,
        LicenseType licenseType,
        uint256 fee
    );
    
    function setLicense(
        uint256 tokenId,
        License calldata license
    ) external;
    
    function purchaseLicense(
        uint256 tokenId,
        LicenseType licenseType
    ) external payable;
    
    function getLicense(
        uint256 tokenId
    ) external view returns (License memory);
    
    function hasActiveLicense(
        uint256 tokenId,
        address licensee,
        LicenseType licenseType
    ) external view returns (bool);
}

Collaborative Creation Extension

interface IMediaCollaboration is ILuxMedia {
    struct Collaborator {
        address addr;
        uint256 share;      // Basis points
        string role;        // Artist, producer, etc.
        bool approved;
    }
    
    struct CollaborativeWork {
        uint256 tokenId;
        address leadCreator;
        Collaborator[] collaborators;
        uint256 totalShares;
        bool finalized;
    }
    
    event CollaboratorAdded(
        uint256 indexed tokenId,
        address indexed collaborator,
        uint256 share,
        string role
    );
    
    event CollaboratorApproved(
        uint256 indexed tokenId,
        address indexed collaborator
    );
    
    event WorkFinalized(
        uint256 indexed tokenId,
        uint256 collaboratorCount
    );
    
    function createCollaborativeWork(
        MediaData calldata data,
        Collaborator[] calldata collaborators
    ) external returns (uint256);
    
    function addCollaborator(
        uint256 tokenId,
        address collaborator,
        uint256 share,
        string calldata role
    ) external;
    
    function approveCollaboration(uint256 tokenId) external;
    
    function finalizeCollaboration(uint256 tokenId) external;
    
    function distributeRoyalties(uint256 tokenId) external;
}

Content Verification Extension

interface IMediaVerification is ILuxMedia {
    struct ContentProof {
        bytes32 contentHash;
        uint256 timestamp;
        string algorithm;       // sha256, keccak256, etc.
        bytes signature;        // Creator's signature
        string ipfsHash;        // IPFS content identifier
    }
    
    event ContentVerified(
        uint256 indexed tokenId,
        bytes32 indexed contentHash,
        address verifier
    );
    
    event ContentDisputed(
        uint256 indexed tokenId,
        address indexed disputer,
        string reason
    );
    
    function registerContentProof(
        uint256 tokenId,
        ContentProof calldata proof
    ) external;
    
    function verifyContent(
        uint256 tokenId,
        bytes calldata content
    ) external view returns (bool);
    
    function disputeContent(
        uint256 tokenId,
        string calldata reason,
        bytes calldata evidence
    ) external;
    
    function getContentProof(
        uint256 tokenId
    ) external view returns (ContentProof memory);
}

Metadata Schema

interface IMediaMetadata {
    struct CoreMetadata {
        string name;
        string description;
        string image;           // Primary image URI
        string animation_url;   // Animation/video URI
        string external_url;    // External link
    }
    
    struct MediaMetadata {
        CoreMetadata core;
        string mimeType;        // Content MIME type
        uint256 size;           // File size in bytes
        uint256 duration;       // Duration in seconds (for time-based media)
        string[] tags;          // Searchable tags
        Properties properties;   // Additional properties
    }
    
    struct Properties {
        uint256 width;          // Pixel width
        uint256 height;         // Pixel height
        uint256 bitrate;        // For audio/video
        string codec;           // Encoding codec
        string colorSpace;      // Color space
        mapping(string => string) custom; // Custom properties
    }
    
    function setMetadata(
        uint256 tokenId,
        MediaMetadata calldata metadata
    ) external;
    
    function getMetadata(
        uint256 tokenId
    ) external view returns (MediaMetadata memory);
    
    function updateProperty(
        uint256 tokenId,
        string calldata key,
        string calldata value
    ) external;
}

Rationale

Content Hash Verification

Using content hashes ensures:

  • Immutable content reference
  • Verification of authenticity
  • Protection against tampering
  • Decentralized storage compatibility

Flexible Royalty System

The bid shares model allows:

  • Creator royalties
  • Previous owner rewards
  • Platform fees
  • Collaborative splits

Licensing Framework

On-chain licensing provides:

  • Clear usage rights
  • Automated payments
  • Legal clarity
  • Commercial opportunities

Backwards Compatibility

This standard extends LRC-721 and is compatible with:

  • Standard NFT marketplaces
  • Existing wallet infrastructure
  • IPFS and Arweave storage
  • Common metadata standards

Test Cases

Media Minting and Trading

contract MediaTest {
    ILuxMedia media;
    
    function testMintMedia() public {
        ILuxMedia.MediaData memory data = ILuxMedia.MediaData({
            tokenURI: "ipfs://QmTokenURI",
            metadataURI: "ipfs://QmMetadataURI",
            contentHash: keccak256("content"),
            metadataHash: keccak256("metadata")
        });
        
        ILuxMedia.BidShares memory shares = ILuxMedia.BidShares({
            creator: 1500,      // 15%
            owner: 7500,        // 75%
            prevOwner: 1000     // 10%
        });
        
        uint256 tokenId = media.mint(data, shares);
        
        // Verify media data
        ILuxMedia.MediaData memory stored = media.tokenMediaData(tokenId);
        assertEq(stored.contentHash, data.contentHash);
        
        // Verify bid shares
        ILuxMedia.BidShares memory storedShares = media.bidSharesForToken(tokenId);
        assertEq(storedShares.creator, 1500);
    }
    
    function testContentVerification() public {
        IMediaVerification verification = IMediaVerification(address(media));
        
        bytes memory content = "Original content";
        bytes32 contentHash = keccak256(content);
        
        IMediaVerification.ContentProof memory proof = IMediaVerification.ContentProof({
            contentHash: contentHash,
            timestamp: block.timestamp,
            algorithm: "keccak256",
            signature: signContent(contentHash),
            ipfsHash: "QmOriginalContent"
        });
        
        verification.registerContentProof(tokenId, proof);
        
        // Verify content
        assertTrue(verification.verifyContent(tokenId, content));
        
        // Try with tampered content
        bytes memory tamperedContent = "Tampered content";
        assertFalse(verification.verifyContent(tokenId, tamperedContent));
    }
}

Collaborative Creation

function testCollaboration() public {
    IMediaCollaboration collab = IMediaCollaboration(address(media));
    
    IMediaCollaboration.Collaborator[] memory collaborators = new IMediaCollaboration.Collaborator[](2);
    collaborators[0] = IMediaCollaboration.Collaborator({
        addr: address(0x1),
        share: 3000,        // 30%
        role: "Artist",
        approved: false
    });
    collaborators[1] = IMediaCollaboration.Collaborator({
        addr: address(0x2),
        share: 2000,        // 20%
        role: "Producer",
        approved: false
    });
    
    uint256 tokenId = collab.createCollaborativeWork(mediaData, collaborators);
    
    // Collaborators approve
    vm.prank(address(0x1));
    collab.approveCollaboration(tokenId);
    
    vm.prank(address(0x2));
    collab.approveCollaboration(tokenId);
    
    // Finalize work
    collab.finalizeCollaboration(tokenId);
    
    // Distribute royalties
    collab.distributeRoyalties(tokenId);
}

Implementation

Reference Implementation

Location: ~/work/lux/standard/src/media-nft/

Files:

  • LuxMedia.sol - Core media NFT implementation
  • ILuxMedia.sol - Media interface
  • IMediaLicensing.sol - Licensing interface
  • IMediaCollaboration.sol - Collaboration interface
  • IMediaVerification.sol - Content verification interface

Deployment:

cd ~/work/lux/standard
forge build

# Deploy to C-Chain
forge script script/DeployMedia.s.sol:DeployMedia \
  --rpc-url https://api.avax.network/ext/bc/C/rpc \
  --broadcast

Testing

Foundry Test Suite: test/media-nft/

cd ~/work/lux/standard

# Run all media NFT tests
forge test --match-path test/media-nft/\* -vvv

# Run specific test
forge test --match MediaTest --match-contract -vvv

# Gas reports
forge test --match-path test/media-nft/\* --gas-report

# Coverage
forge coverage --match-path test/media-nft/\*

Test Cases (see /test/media-nft/LuxMedia.t.sol):

  • testMintMedia() - Mint with metadata and bid shares
  • testContentVerification() - Content hash verification
  • testCollaboration() - Multi-creator collaboration workflow
  • testLicensing() - License setup and activation
  • testBidding() - Ask/bid mechanics
  • testRoyaltiesDistribution() - Automatic royalty splits
  • testMetadata() - Rich metadata handling
  • testCollaborativeWork() - Finalization and approval

Gas Benchmarks (Apple M1 Max):

OperationGas CostTime
mint~140,000~3.5ms
mintWithSig~165,000~4.1ms
setAsk~55,000~1.4ms
setBid~85,000~2.1ms
acceptBid~125,000~3.1ms
setLicense~70,000~1.7ms

Contract Verification

Etherscan/Sourcify:

forge verify-contract \
  --chain-id 43114 \
  --watch 0x<MEDIA_ADDRESS> \
  src/media-nft/LuxMedia.sol:LuxMedia

Reference Implementation

contract LuxMedia is ILuxMedia, IMediaLicensing, ERC721 {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIdCounter;
    
    mapping(uint256 => MediaData) private _tokenMediaData;
    mapping(uint256 => BidShares) private _bidShares;
    mapping(uint256 => Ask) private _asks;
    mapping(uint256 => Bid) private _bids;
    mapping(uint256 => License) private _licenses;
    
    address public marketContract;
    
    modifier onlyTokenCreatorOrOwner(uint256 tokenId) {
        require(
            _creators[tokenId] == msg.sender || ownerOf(tokenId) == msg.sender,
            "Not creator or owner"
        );
        _;
    }
    
    function mint(
        MediaData calldata data,
        BidShares calldata bidShares
    ) external override returns (uint256) {
        require(
            data.contentHash != bytes32(0) && data.metadataHash != bytes32(0),
            "Invalid hashes"
        );
        require(
            bidShares.creator + bidShares.owner + bidShares.prevOwner == 10000,
            "Invalid shares"
        );
        
        _tokenIdCounter.increment();
        uint256 tokenId = _tokenIdCounter.current();
        
        _safeMint(msg.sender, tokenId);
        _setTokenMediaData(tokenId, data);
        _setBidShares(tokenId, bidShares);
        _creators[tokenId] = msg.sender;
        
        emit MediaMinted(
            tokenId,
            msg.sender,
            data.tokenURI,
            data.metadataURI,
            data.contentHash,
            data.metadataHash
        );
        
        return tokenId;
    }
    
    function updateTokenURI(
        uint256 tokenId,
        string calldata tokenURI
    ) external override onlyTokenCreatorOrOwner(tokenId) {
        _tokenMediaData[tokenId].tokenURI = tokenURI;
        
        emit MediaUpdated(
            tokenId,
            ownerOf(tokenId),
            tokenURI,
            _tokenMediaData[tokenId].metadataURI,
            _tokenMediaData[tokenId].contentHash,
            _tokenMediaData[tokenId].metadataHash
        );
    }
    
    function setAsk(uint256 tokenId, Ask calldata ask) external override {
        require(ownerOf(tokenId) == msg.sender, "Not owner");
        require(ask.amount > 0, "Invalid amount");
        
        _asks[tokenId] = ask;
        emit AskCreated(tokenId, ask);
    }
    
    function setBid(uint256 tokenId, Bid calldata bid) external override {
        require(bid.bidder == msg.sender, "Invalid bidder");
        require(bid.amount > 0, "Invalid amount");
        require(bid.expiry > block.timestamp, "Expired");
        
        // Handle bid currency transfer
        if (bid.currency == address(0)) {
            require(msg.value == bid.amount, "Invalid payment");
        } else {
            IERC20(bid.currency).transferFrom(msg.sender, address(this), bid.amount);
        }
        
        _bids[tokenId] = bid;
        emit BidCreated(tokenId, bid);
    }
    
    function acceptBid(uint256 tokenId, Bid calldata expectedBid) external override {
        require(ownerOf(tokenId) == msg.sender, "Not owner");
        
        Bid memory bid = _bids[tokenId];
        require(
            bid.amount == expectedBid.amount &&
            bid.currency == expectedBid.currency &&
            bid.bidder == expectedBid.bidder,
            "Bid mismatch"
        );
        require(bid.expiry >= block.timestamp, "Bid expired");
        
        // Calculate distributions
        BidShares memory shares = _bidShares[tokenId];
        uint256 creatorShare = (bid.amount * shares.creator) / 10000;
        uint256 prevOwnerShare = (bid.amount * shares.prevOwner) / 10000;
        uint256 ownerShare = bid.amount - creatorShare - prevOwnerShare;
        
        // Transfer NFT
        _transfer(msg.sender, bid.recipient, tokenId);
        
        // Distribute payments
        _handlePayment(bid.currency, _creators[tokenId], creatorShare);
        _handlePayment(bid.currency, _previousOwners[tokenId], prevOwnerShare);
        _handlePayment(bid.currency, msg.sender, ownerShare);
        
        // Clear bid
        delete _bids[tokenId];
        _previousOwners[tokenId] = msg.sender;
        
        emit BidFinalized(tokenId, bid);
    }
    
    function setLicense(
        uint256 tokenId,
        License calldata license
    ) external override onlyTokenCreatorOrOwner(tokenId) {
        _licenses[tokenId] = license;
        emit LicenseSet(tokenId, license.licenseType, license.licenseURI);
    }
    
    function _setTokenMediaData(uint256 tokenId, MediaData calldata data) internal {
        _tokenMediaData[tokenId] = data;
    }
    
    function _setBidShares(uint256 tokenId, BidShares calldata bidShares) internal {
        _bidShares[tokenId] = bidShares;
    }
    
    function _handlePayment(address currency, address recipient, uint256 amount) internal {
        if (amount == 0 || recipient == address(0)) return;
        
        if (currency == address(0)) {
            payable(recipient).transfer(amount);
        } else {
            IERC20(currency).transfer(recipient, amount);
        }
    }
}

Security Considerations

Content Hash Validation

Always verify content hashes:

require(keccak256(content) == contentHash, "Content mismatch");

Royalty Distribution

Ensure safe transfers:

(bool success, ) = recipient.call{value: amount}("");
require(success, "Transfer failed");

License Enforcement

Validate license terms:

require(block.timestamp < license.expirationTime, "License expired");

Collaborative Work

Require all approvals:

for (uint i = 0; i < collaborators.length; i++) {
    require(collaborators[i].approved, "Not all approved");
}

Copyright and related rights waived via CC0.```